2015-05-21 22:33:41 +03:00
/*
* Copyright ( C ) 2015 Red Hat , Inc .
*
* This program is free software ; you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation ; either version 2 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with this program ; if not , write to the Free Software
* Foundation , Inc . , 51 Franklin St , Fifth Floor , Boston , MA 02110 - 1301 USA
*/
2015-06-10 01:57:32 +03:00
# include "config.h"
2015-08-24 22:44:40 +03:00
# include "rpmostreed-utils.h"
# include "rpmostreed-errors.h"
2016-11-06 02:48:32 +03:00
# include "libglnx.h"
Introduce `ex livefs`
There are a few different use cases here. First, for layering new packages,
there's no good reason for us to force a reboot. Second, we want some support
for cherry-picking security updates and allowing admins to restart services. Finally,
at some point we should offer support for entirely replacing the running tree
if that's what the user wants.
Until now we've been very conservative, but there's a spectrum here. In
particular, this patch changes things so we push a rollback before we start
doing anything live. I think in practice, many use cases would be totally fine
with doing most changes live, and falling back to the rollback if something went
wrong.
This initial code drop *only* supports live layering of new packages. However,
a lot of the base infrastructure is laid for future work.
For now, this will be classified as an experimental feature, hence `ex livefs`.
Part of: https://github.com/projectatomic/rpm-ostree/issues/639
Closes: #652
Approved by: jlebon
2017-03-01 01:16:48 +03:00
# include <systemd/sd-journal.h>
# include <stdint.h>
2015-05-21 22:33:41 +03:00
2015-06-10 01:57:32 +03:00
# include <libglnx.h>
2015-05-22 21:23:57 +03:00
2015-05-25 20:25:05 +03:00
static void
append_to_object_path ( GString * str ,
const gchar * s )
{
guint n ;
for ( n = 0 ; s [ n ] ! = ' \0 ' ; n + + )
{
gint c = s [ n ] ;
/* D-Bus spec sez:
*
* Each element must only contain the ASCII characters " [A-Z][a-z][0-9]_- "
*/
if ( ( c > = ' A ' & & c < = ' Z ' ) | | ( c > = ' a ' & & c < = ' z ' ) | | ( c > = ' 0 ' & & c < = ' 9 ' ) | | c = = ' _ ' )
{
g_string_append_c ( str , c ) ;
}
else if ( c = = ' - ' | | c = = ' / ' )
{
/* Swap / or - for _ to keep names easier to read */
g_string_append_c ( str , ' _ ' ) ;
}
else
{
/* Escape bytes not in [A-Z][a-z][0-9] as _<hex-with-two-digits> */
g_string_append_printf ( str , " _%02x " , c & 0xFF ) ;
}
}
}
/**
2015-08-24 22:44:40 +03:00
* rpmostreed_generate_object_path :
2015-05-25 20:25:05 +03:00
* @ base : The base object path ( without trailing ' / ' ) .
* @ part . . . : UTF - 8 strings .
*
* Appends @ s to @ base in a way such that only characters that can be
* used in a D - Bus object path will be used . E . g . a character not in
* < literal > [ A - Z ] [ a - z ] [ 0 - 9 ] _ < / literal > will be escaped as _HEX where
* HEX is a two - digit hexadecimal number .
*
* Note that his mapping is not bijective - e . g . you cannot go back
* to the original string .
*
* Returns : An allocated string that must be freed with g_free ( ) .
*/
gchar *
2015-08-24 22:44:40 +03:00
rpmostreed_generate_object_path ( const gchar * base ,
const gchar * part ,
. . . )
2015-05-25 20:25:05 +03:00
{
gchar * result ;
va_list va ;
va_start ( va , part ) ;
2015-08-24 22:44:40 +03:00
result = rpmostreed_generate_object_path_from_va ( base , part , va ) ;
2015-05-25 20:25:05 +03:00
va_end ( va ) ;
return result ;
}
gchar *
2015-08-24 22:44:40 +03:00
rpmostreed_generate_object_path_from_va ( const gchar * base ,
const gchar * part ,
va_list va )
2015-05-25 20:25:05 +03:00
{
GString * path ;
g_return_val_if_fail ( base ! = NULL , NULL ) ;
g_return_val_if_fail ( g_variant_is_object_path ( base ) , NULL ) ;
g_return_val_if_fail ( ! g_str_has_suffix ( base , " / " ) , NULL ) ;
path = g_string_new ( base ) ;
while ( part ! = NULL )
{
if ( ! g_utf8_validate ( part , - 1 , NULL ) )
{
g_string_free ( path , TRUE ) ;
return NULL ;
}
else
{
g_string_append_c ( path , ' / ' ) ;
append_to_object_path ( path , part ) ;
part = va_arg ( va , const gchar * ) ;
}
}
return g_string_free ( path , FALSE ) ;
}
2015-06-11 05:17:49 +03:00
/**
2015-08-24 22:44:40 +03:00
* rpmostreed_refspec_parse_partial :
2015-06-11 05:17:49 +03:00
* @ new_provided_refspec : The provided refspec
* @ base_refspec : The refspec string to base on .
* @ out_refspec : Pointer to the new refspec
* @ error : Pointer to an error pointer .
*
* Takes a refspec string and adds any missing bits based on the
* base_refspec argument . Errors if a full valid refspec can ' t
* be derived .
*
2015-08-24 22:44:40 +03:00
* Returns : True on success .
2015-06-11 05:17:49 +03:00
*/
gboolean
2015-08-24 22:44:40 +03:00
rpmostreed_refspec_parse_partial ( const gchar * new_provided_refspec ,
2015-02-11 21:06:43 +03:00
const gchar * base_refspec ,
2015-08-24 22:44:40 +03:00
gchar * * out_refspec ,
GError * * error )
2015-06-11 05:17:49 +03:00
{
g_autofree gchar * ref = NULL ;
g_autofree gchar * remote = NULL ;
2017-05-05 22:40:55 +03:00
gboolean infer_remote = TRUE ; ;
2015-06-11 05:17:49 +03:00
/* Allow just switching remotes */
if ( g_str_has_suffix ( new_provided_refspec , " : " ) )
{
2017-05-05 22:39:01 +03:00
remote = g_strndup ( new_provided_refspec , strlen ( new_provided_refspec ) - 1 ) ;
2015-06-11 05:17:49 +03:00
}
2017-05-05 22:40:55 +03:00
/* Allow switching to a local branch */
else if ( g_str_has_prefix ( new_provided_refspec , " : " ) )
{
infer_remote = FALSE ;
ref = g_strdup ( new_provided_refspec + 1 ) ;
}
2015-06-11 05:17:49 +03:00
else
{
2017-05-05 22:39:01 +03:00
g_autoptr ( GError ) parse_error = NULL ;
2015-06-11 05:17:49 +03:00
if ( ! ostree_parse_refspec ( new_provided_refspec , & remote ,
& ref , & parse_error ) )
{
g_set_error_literal ( error , RPM_OSTREED_ERROR ,
2017-05-05 22:39:01 +03:00
RPM_OSTREED_ERROR_INVALID_REFSPEC ,
parse_error - > message ) ;
return FALSE ;
2015-06-11 05:17:49 +03:00
}
}
2017-05-05 22:39:01 +03:00
g_autofree gchar * origin_ref = NULL ;
g_autofree gchar * origin_remote = NULL ;
2015-06-11 05:17:49 +03:00
if ( base_refspec ! = NULL )
{
2017-05-05 22:39:01 +03:00
g_autoptr ( GError ) parse_error = NULL ;
2015-06-11 05:17:49 +03:00
if ( ! ostree_parse_refspec ( base_refspec , & origin_remote ,
2017-05-05 22:39:01 +03:00
& origin_ref , & parse_error ) )
{
g_set_error_literal ( error , RPM_OSTREED_ERROR ,
RPM_OSTREED_ERROR_INVALID_REFSPEC ,
parse_error - > message ) ;
return FALSE ;
}
2015-06-11 05:17:49 +03:00
}
if ( ref = = NULL )
{
if ( origin_ref )
{
ref = g_strdup ( origin_ref ) ;
}
else
{
g_set_error ( error , RPM_OSTREED_ERROR ,
RPM_OSTREED_ERROR_INVALID_REFSPEC ,
" Could not determine default ref to pull. " ) ;
2017-05-05 22:39:01 +03:00
return FALSE ;
2015-06-11 05:17:49 +03:00
}
}
2017-05-05 22:40:55 +03:00
else if ( infer_remote & & remote = = NULL )
2015-06-11 05:17:49 +03:00
{
if ( origin_remote )
{
remote = g_strdup ( origin_remote ) ;
}
}
if ( g_strcmp0 ( origin_remote , remote ) = = 0 & &
g_strcmp0 ( origin_ref , ref ) = = 0 )
{
g_set_error ( error , RPM_OSTREED_ERROR ,
RPM_OSTREED_ERROR_INVALID_REFSPEC ,
" Old and new refs are equal: %s:%s " ,
remote , ref ) ;
2017-05-05 22:39:01 +03:00
return FALSE ;
2015-06-11 05:17:49 +03:00
}
2016-06-30 22:50:57 +03:00
if ( remote = = NULL )
* out_refspec = g_steal_pointer ( & ref ) ;
else
* out_refspec = g_strconcat ( remote , " : " , ref , NULL ) ;
2017-05-05 22:39:01 +03:00
return TRUE ;
2015-05-25 20:25:05 +03:00
}
2015-10-07 09:22:36 +03:00
void
rpmostreed_reboot ( GCancellable * cancellable , GError * * error )
{
2016-10-12 22:17:14 +03:00
const char * child_argv [ ] = { " systemctl " , " reboot " , NULL } ;
( void ) g_spawn_sync ( NULL , ( char * * ) child_argv , NULL , G_SPAWN_CHILD_INHERITS_STDIN | G_SPAWN_SEARCH_PATH ,
2017-06-22 00:09:03 +03:00
NULL , NULL , NULL , NULL , NULL , NULL ) ;
2015-10-07 09:22:36 +03:00
}
2015-10-09 23:47:39 +03:00
/**
* rpmostreed_repo_pull_ancestry :
* @ repo : Repo
* @ refspec : Repository branch
* @ visitor : ( allow - none ) : Visitor function to call on each commit
* @ visitor_data : ( allow - none ) : User data for @ visitor
* @ progress : ( allow - none ) : Progress
* @ cancellable : Cancellable
* @ error : Error
*
* Downloads an ancestry of commit objects starting from @ refspec .
*
* If a @ visitor function pointer is given , commit objects are downloaded
* in batches and the @ visitor function is called for each commit object .
* The @ visitor function can stop the recursion , such as when looking for
* a particular commit .
*
* Returns : % TRUE on success , % FALSE on failure
*/
gboolean
rpmostreed_repo_pull_ancestry ( OstreeRepo * repo ,
const char * refspec ,
RpmostreedCommitVisitor visitor ,
gpointer visitor_data ,
OstreeAsyncProgress * progress ,
GCancellable * cancellable ,
GError * * error )
{
OstreeRepoPullFlags flags ;
GVariantDict options ;
GVariant * refs_value ;
const char * refs_array [ ] = { NULL , NULL } ;
g_autofree char * remote = NULL ;
g_autofree char * ref = NULL ;
g_autofree char * checksum = NULL ;
int depth , ii ;
gboolean ret = FALSE ;
2016-12-22 20:39:28 +03:00
/* Only fetch the HEAD on the first pass. See also:
* https : //github.com/projectatomic/rpm-ostree/pull/557 */
gboolean first_pass = TRUE ;
2015-10-09 23:47:39 +03:00
g_return_val_if_fail ( OSTREE_IS_REPO ( repo ) , FALSE ) ;
g_return_val_if_fail ( refspec ! = NULL , FALSE ) ;
if ( ! ostree_parse_refspec ( refspec , & remote , & ref , error ) )
goto out ;
/* If no visitor function was provided then we won't be short-circuiting
* the recursion , so pull everything in one shot . Otherwise pull commits
* in increasingly large batches . */
depth = ( visitor ! = NULL ) ? 10 : - 1 ;
flags = OSTREE_REPO_PULL_FLAGS_COMMIT_ONLY ;
/* It's important to use the ref name instead of a checksum on the first
* pass because we want to search from the latest available commit on the
* remote server , which is not necessarily what the ref name is currently
* pointing at in our local repo . */
refs_array [ 0 ] = ref ;
while ( TRUE )
{
2016-06-30 19:41:20 +03:00
if ( remote ! = NULL )
{
/* Floating reference, transferred to dictionary. */
refs_value =
g_variant_new_strv ( ( const char * const * ) refs_array , - 1 ) ;
g_variant_dict_init ( & options , NULL ) ;
2016-12-22 20:39:28 +03:00
if ( ! first_pass )
g_variant_dict_insert ( & options , " depth " , " i " , depth ) ;
2016-06-30 19:41:20 +03:00
g_variant_dict_insert ( & options , " flags " , " i " , flags ) ;
g_variant_dict_insert_value ( & options , " refs " , refs_value ) ;
if ( ! ostree_repo_pull_with_options ( repo , remote ,
g_variant_dict_end ( & options ) ,
progress , cancellable , error ) )
goto out ;
}
2015-10-09 23:47:39 +03:00
/* First pass only. Now we can resolve the ref to a checksum. */
if ( checksum = = NULL )
{
2016-10-31 19:19:08 +03:00
if ( ! ostree_repo_resolve_rev ( repo , refspec , FALSE , & checksum , error ) )
2015-10-09 23:47:39 +03:00
goto out ;
}
2016-06-30 19:41:20 +03:00
if ( visitor ! = NULL )
2015-10-09 23:47:39 +03:00
{
2016-12-22 20:39:28 +03:00
for ( ii = 0 ; ii < ( first_pass ? 1 : depth ) & & checksum ! = NULL ; ii + + )
2016-06-30 19:41:20 +03:00
{
g_autoptr ( GVariant ) commit = NULL ;
gboolean stop = FALSE ;
2015-10-09 23:47:39 +03:00
2016-06-30 19:41:20 +03:00
if ( ! ostree_repo_load_commit ( repo , checksum , & commit ,
NULL , error ) )
goto out ;
2015-10-09 23:47:39 +03:00
2016-06-30 19:41:20 +03:00
if ( ! visitor ( repo , checksum , commit , visitor_data , & stop , error ) )
goto out ;
2015-10-09 23:47:39 +03:00
2016-06-30 19:41:20 +03:00
g_clear_pointer ( & checksum , g_free ) ;
2015-10-09 23:47:39 +03:00
2016-06-30 19:41:20 +03:00
if ( ! stop )
checksum = ostree_commit_get_parent ( commit ) ;
}
2015-10-09 23:47:39 +03:00
}
/* Break if no visitor, or visitor told us to stop. */
2016-06-30 19:41:20 +03:00
if ( visitor = = NULL | | checksum = = NULL )
2015-10-09 23:47:39 +03:00
break ;
/* Pull the next batch of commits, twice as many. */
refs_array [ 0 ] = checksum ;
2016-12-22 20:39:28 +03:00
if ( ! first_pass )
depth = depth * 2 ;
first_pass = FALSE ;
2015-10-09 23:47:39 +03:00
}
ret = TRUE ;
out :
return ret ;
}
typedef struct {
const char * version ;
char * checksum ;
} VersionVisitorClosure ;
static gboolean
version_visitor ( OstreeRepo * repo ,
const char * checksum ,
GVariant * commit ,
gpointer user_data ,
gboolean * out_stop ,
GError * * error )
{
VersionVisitorClosure * closure = user_data ;
g_autoptr ( GVariant ) metadict = NULL ;
const char * version = NULL ;
metadict = g_variant_get_child_value ( commit , 0 ) ;
if ( g_variant_lookup ( metadict , " version " , " &s " , & version ) )
{
if ( g_str_equal ( version , closure - > version ) )
{
closure - > checksum = g_strdup ( checksum ) ;
* out_stop = TRUE ;
}
}
return TRUE ;
}
/**
* rpmostreed_repo_lookup_version :
* @ repo : Repo
* @ refspec : Repository branch
* @ version : Version to look for
* @ progress : ( allow - none ) : Progress
* @ cancellable : Cancellable
* @ out_checksum : ( out ) ( allow - none ) : Commit checksum , or % NULL
* @ error : Error
*
* Tries to determine the commit checksum for @ version on @ refspec .
* This may require pulling commit objects from a remote repository .
*
* Returns : % TRUE on success , % FALSE on failure
*/
gboolean
rpmostreed_repo_lookup_version ( OstreeRepo * repo ,
const char * refspec ,
const char * version ,
OstreeAsyncProgress * progress ,
GCancellable * cancellable ,
char * * out_checksum ,
GError * * error )
{
VersionVisitorClosure closure = { version , NULL } ;
gboolean ret = FALSE ;
g_return_val_if_fail ( OSTREE_IS_REPO ( repo ) , FALSE ) ;
g_return_val_if_fail ( refspec ! = NULL , FALSE ) ;
g_return_val_if_fail ( version ! = NULL , FALSE ) ;
if ( ! rpmostreed_repo_pull_ancestry ( repo , refspec ,
version_visitor , & closure ,
progress , cancellable , error ) )
goto out ;
if ( closure . checksum = = NULL )
{
g_set_error ( error , G_IO_ERROR , G_IO_ERROR_NOT_FOUND ,
" Version %s not found in %s " , version , refspec ) ;
goto out ;
}
if ( out_checksum ! = NULL )
* out_checksum = g_steal_pointer ( & closure . checksum ) ;
g_free ( closure . checksum ) ;
ret = TRUE ;
out :
return ret ;
}
2015-10-22 19:22:56 +03:00
2016-10-18 00:18:11 +03:00
typedef struct {
const char * wanted_checksum ;
gboolean found ;
} ChecksumVisitorClosure ;
static gboolean
checksum_visitor ( OstreeRepo * repo ,
const char * checksum ,
GVariant * commit ,
gpointer user_data ,
gboolean * out_stop ,
GError * * error )
{
ChecksumVisitorClosure * closure = user_data ;
* out_stop = closure - > found = g_str_equal ( checksum , closure - > wanted_checksum ) ;
return TRUE ;
}
/**
* rpmostreed_repo_lookup_checksum :
* @ repo : Repo
* @ refspec : Repository branch
* @ checksum : Checksum to look for
* @ progress : ( allow - none ) : Progress
* @ cancellable : Cancellable
* @ error : Error
*
* Tries to determine if the checksum given belongs on the remote and branch
* given by @ refspec . This may require pulling commit objects from a remote
* repository .
*
* Returns : % TRUE on success , % FALSE on failure
*/
gboolean
rpmostreed_repo_lookup_checksum ( OstreeRepo * repo ,
const char * refspec ,
const char * checksum ,
OstreeAsyncProgress * progress ,
GCancellable * cancellable ,
GError * * error )
{
ChecksumVisitorClosure closure = { checksum , FALSE } ;
g_return_val_if_fail ( OSTREE_IS_REPO ( repo ) , FALSE ) ;
g_return_val_if_fail ( refspec ! = NULL , FALSE ) ;
g_return_val_if_fail ( checksum ! = NULL , FALSE ) ;
if ( ! rpmostreed_repo_pull_ancestry ( repo , refspec ,
checksum_visitor , & closure ,
progress , cancellable , error ) )
return FALSE ;
if ( ! closure . found )
{
g_set_error ( error , G_IO_ERROR , G_IO_ERROR_NOT_FOUND ,
" Checksum %s not found in %s " , checksum , refspec ) ;
return FALSE ;
}
return TRUE ;
}
2015-10-22 19:22:56 +03:00
/**
* rpmostreed_repo_lookup_cached_version :
* @ repo : Repo
* @ refspec : Repository branch
* @ version : Version to look for
* @ cancellable : Cancellable
* @ out_checksum : ( out ) ( allow - none ) : Commit checksum , or % NULL
* @ error : Error
*
* Similar to rpmostreed_repo_lookup_version ( ) , except without pulling
* from a remote repository . It traverses whatever commits are available
* locally in @ repo .
*
* Returns : % TRUE on success , % FALSE on failure
*/
gboolean
rpmostreed_repo_lookup_cached_version ( OstreeRepo * repo ,
const char * refspec ,
const char * version ,
GCancellable * cancellable ,
char * * out_checksum ,
GError * * error )
{
VersionVisitorClosure closure = { version , NULL } ;
g_autofree char * checksum = NULL ;
gboolean ret = FALSE ;
g_return_val_if_fail ( OSTREE_IS_REPO ( repo ) , FALSE ) ;
g_return_val_if_fail ( refspec ! = NULL , FALSE ) ;
g_return_val_if_fail ( version ! = NULL , FALSE ) ;
if ( ! ostree_repo_resolve_rev ( repo , refspec , FALSE , & checksum , error ) )
goto out ;
while ( checksum ! = NULL )
{
g_autoptr ( GVariant ) commit = NULL ;
gboolean stop = FALSE ;
if ( ! ostree_repo_load_commit ( repo , checksum , & commit , NULL , error ) )
goto out ;
if ( ! version_visitor ( repo , checksum , commit , & closure , & stop , error ) )
goto out ;
g_clear_pointer ( & checksum , g_free ) ;
if ( ! stop )
checksum = ostree_commit_get_parent ( commit ) ;
}
if ( closure . checksum = = NULL )
{
g_set_error ( error , G_IO_ERROR , G_IO_ERROR_NOT_FOUND ,
" Version %s not cached in %s " , version , refspec ) ;
goto out ;
}
if ( out_checksum ! = NULL )
* out_checksum = g_steal_pointer ( & closure . checksum ) ;
g_free ( closure . checksum ) ;
ret = TRUE ;
out :
return ret ;
}
2015-11-05 05:14:14 +03:00
/**
* rpmostreed_parse_revision :
* @ revision : Revision string
* @ out_checksum : ( out ) ( allow - none ) : Commit checksum , or % NULL
* @ out_version : ( out ) ( allow - none ) : Version value , or % NULL
* @ error : Error
*
* Determines @ revision to either be a SHA256 checksum or a version metadata
* value , sets one of @ out_checksum or @ out_version appropriately , and then
* returns % TRUE .
*
* The @ revision string may have a " revision= " prefix to denote a SHA256
* checksum , or a " version= " prefix to denote a version metadata value . If
* the @ revision string lacks either prefix , the function attempts to infer
* the type of revision . The prefixes are case - insensitive .
*
* The only possible error is if a " revision= " prefix is given , but the
* rest of the string is not a valid SHA256 checksum . In that case the
* function sets @ error and returns % FALSE .
*
* Returns : % TRUE on success , % FALSE of failure
*/
gboolean
rpmostreed_parse_revision ( const char * revision ,
char * * out_checksum ,
char * * out_version ,
GError * * error )
{
const char * checksum = NULL ;
const char * version = NULL ;
gboolean ret = FALSE ;
g_return_val_if_fail ( revision ! = NULL , FALSE ) ;
if ( g_ascii_strncasecmp ( revision , " revision= " , 9 ) = = 0 )
{
checksum = revision + 9 ;
/* Since this claims to be a checksum, fail if it isn't. */
if ( ! ostree_validate_checksum_string ( checksum , error ) )
goto out ;
}
else if ( g_ascii_strncasecmp ( revision , " version= " , 8 ) = = 0 )
{
version = revision + 8 ;
}
else if ( ostree_validate_checksum_string ( revision , NULL ) )
{
/* If it looks like a checksum, assume it is. */
checksum = revision ;
}
else
{
/* Treat anything else as a version metadata value. */
version = revision ;
}
if ( out_checksum ! = NULL )
* out_checksum = g_strdup ( checksum ) ;
if ( out_version ! = NULL )
* out_version = g_strdup ( version ) ;
ret = TRUE ;
out :
return ret ;
}