2014-01-24 22:34:19 +04:00
/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
*
* Copyright ( C ) 2013 , 2014 Colin Walters < walters @ verbum . org >
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation ; either version 2 of the licence or ( at
* your option ) any later version .
*
* This library 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
* Lesser General Public License for more details .
*
* You should have received a copy of the GNU Lesser General
* Public License along with this library ; if not , write to the
* Free Software Foundation , Inc . , 59 Temple Place , Suite 330 ,
* Boston , MA 02111 - 1307 , USA .
*/
# include "config.h"
# include <string.h>
# include <glib-unix.h>
2014-02-13 03:26:31 +04:00
# include <json-glib/json-glib.h>
2014-05-03 18:05:43 +04:00
# include <gio/gunixoutputstream.h>
2015-01-01 02:18:33 +03:00
# include <libhif.h>
2015-02-03 13:07:20 +03:00
# include <libhif/hif-utils.h>
2014-11-10 04:28:10 +03:00
# include <stdio.h>
2015-02-15 21:56:14 +03:00
# include <libglnx.h>
2015-01-02 01:44:57 +03:00
# include <rpm/rpmmacro.h>
2014-01-24 22:34:19 +04:00
2014-05-26 23:05:08 +04:00
# include "rpmostree-compose-builtins.h"
2014-05-16 01:46:51 +04:00
# include "rpmostree-util.h"
2014-11-13 22:53:43 +03:00
# include "rpmostree-json-parsing.h"
2014-11-12 02:40:58 +03:00
# include "rpmostree-cleanup.h"
2014-10-31 06:49:18 +03:00
# include "rpmostree-treepkgdiff.h"
compose: Introduce a little 'libcontainer', use it for the post script
The current motivation for this is that
https://github.com/fedora-infra/fedmsg-atomic-composer
started using mock --new-chroot (which uses systemd-nspawn) to run
rpm-ostree, which in turn uses systemd-nspawn to run the post script.
Now systemd-nspawn is not really nestable (it wants to link up
journald, resolv.conf handling, etc).
First, dropping nspawn and going to raw containers fixes the nesting
problem.
Second, we don't need all the features of systemd-nspawn. We are ok
with log messages going to stdout, and we don't use networking, so no
resolv.conf is needed.
Third, this sets a bit of a stage for more sandboxing internally when
run on real systems. I already have a prototype branch which runs
librepo as an unprivileged user, that could be combined with this for
even stronger security.
Why not use systemd? Well...I'm still debating that. But the core
problem is systemd isn't a library in the C sense - to use its
sandboxing features we have to use unit files. It's harder to have a
daemon that looks like a single service from a management perspective,
but uses sandboxing internally.
2014-11-20 02:30:00 +03:00
# include "rpmostree-libcontainer.h"
2014-01-29 23:37:44 +04:00
# include "rpmostree-postprocess.h"
2014-11-20 17:40:35 +03:00
# include "rpmostree-passwd-util.h"
2014-01-29 23:37:44 +04:00
2014-01-24 22:34:19 +04:00
# include "libgsystem.h"
static char * opt_workdir ;
2014-07-02 06:07:56 +04:00
static gboolean opt_workdir_tmpfs ;
2014-05-03 14:55:35 +04:00
static char * opt_cachedir ;
2015-01-03 05:54:23 +03:00
static gboolean opt_force_nocache ;
2014-05-03 14:55:35 +04:00
static char * opt_proxy ;
2014-10-24 01:14:14 +04:00
static char * opt_output_repodata_dir ;
2014-10-14 17:32:29 +04:00
static char * * opt_metadata_strings ;
2014-05-03 14:55:35 +04:00
static char * opt_repo ;
2014-07-11 22:02:45 +04:00
static char * * opt_override_pkg_repos ;
2015-01-29 00:34:29 +03:00
static char * opt_touch_if_changed ;
2014-05-03 18:05:43 +04:00
static gboolean opt_print_only ;
2014-01-24 22:34:19 +04:00
static GOptionEntry option_entries [ ] = {
2014-10-14 17:32:29 +04:00
{ " add-metadata-string " , 0 , 0 , G_OPTION_ARG_STRING_ARRAY , & opt_metadata_strings , " Append given key and value (in string format) to metadata " , " KEY=VALUE " } ,
2014-05-03 14:55:35 +04:00
{ " workdir " , 0 , 0 , G_OPTION_ARG_STRING , & opt_workdir , " Working directory " , " WORKDIR " } ,
2014-07-02 06:07:56 +04:00
{ " workdir-tmpfs " , 0 , 0 , G_OPTION_ARG_NONE , & opt_workdir_tmpfs , " Use tmpfs for working state " , NULL } ,
2014-10-24 01:14:14 +04:00
{ " output-repodata-dir " , 0 , 0 , G_OPTION_ARG_STRING , & opt_output_repodata_dir , " Save downloaded repodata in DIR " , " DIR " } ,
2014-05-03 14:55:35 +04:00
{ " cachedir " , 0 , 0 , G_OPTION_ARG_STRING , & opt_cachedir , " Cached state " , " CACHEDIR " } ,
2015-01-03 05:54:23 +03:00
{ " force-nocache " , 0 , 0 , G_OPTION_ARG_NONE , & opt_force_nocache , " Always create a new OSTree commit, even if nothing appears to have changed " , NULL } ,
2014-05-03 14:55:35 +04:00
{ " repo " , ' r ' , 0 , G_OPTION_ARG_STRING , & opt_repo , " Path to OSTree repository " , " REPO " } ,
2014-07-11 22:02:45 +04:00
{ " add-override-pkg-repo " , 0 , 0 , G_OPTION_ARG_STRING_ARRAY , & opt_override_pkg_repos , " Include an additional package repository from DIRECTORY " , " DIRECTORY " } ,
2014-05-03 14:55:35 +04:00
{ " proxy " , 0 , 0 , G_OPTION_ARG_STRING , & opt_proxy , " HTTP proxy " , " PROXY " } ,
2015-01-29 00:34:29 +03:00
{ " touch-if-changed " , 0 , 0 , G_OPTION_ARG_STRING , & opt_touch_if_changed , " Update the modification time on FILE if a new commit was created " , " FILE " } ,
2014-05-03 18:05:43 +04:00
{ " print-only " , 0 , 0 , G_OPTION_ARG_NONE , & opt_print_only , " Just expand any includes and print treefile " , NULL } ,
2014-01-24 22:34:19 +04:00
{ NULL }
} ;
2015-01-07 09:30:27 +03:00
/* FIXME: This is a copy of ot_admin_checksum_version */
static char *
checksum_version ( GVariant * checksum )
{
gs_unref_variant GVariant * metadata = NULL ;
const char * ret = NULL ;
metadata = g_variant_get_child_value ( checksum , 0 ) ;
if ( ! g_variant_lookup ( metadata , " version " , " &s " , & ret ) )
return NULL ;
return g_strdup ( ret ) ;
}
2014-05-18 22:13:31 +04:00
typedef struct {
GPtrArray * treefile_context_dirs ;
2014-11-17 04:05:47 +03:00
GFile * workdir ;
2015-02-15 21:56:14 +03:00
int workdir_dfd ;
2015-01-03 05:54:23 +03:00
OstreeRepo * repo ;
char * previous_checksum ;
2014-09-07 20:38:34 +04:00
GBytes * serialized_treefile ;
2014-05-18 22:13:31 +04:00
} RpmOstreeTreeComposeContext ;
2015-01-03 05:54:23 +03:00
static int
ptrarray_sort_compare_strings ( gconstpointer ap ,
gconstpointer bp )
{
char * * asp = ( gpointer ) ap ;
char * * bsp = ( gpointer ) bp ;
return strcmp ( * asp , * bsp ) ;
}
static gboolean
compute_checksum_from_treefile_and_goal ( RpmOstreeTreeComposeContext * self ,
HyGoal goal ,
char * * out_checksum ,
GError * * error )
{
gboolean ret = FALSE ;
gs_free char * ret_checksum = NULL ;
GChecksum * checksum = g_checksum_new ( G_CHECKSUM_SHA256 ) ;
/* Hash in the raw treefile; this means reordering the input packages
* or adding a comment will cause a recompose , but let ' s be conservative
* here .
*/
{ gsize len ;
const guint8 * buf = g_bytes_get_data ( self - > serialized_treefile , & len ) ;
g_checksum_update ( checksum , buf , len ) ;
}
/* FIXME; we should also hash the post script */
/* Hash in each package */
{ _cleanup_hypackagelist_ HyPackageList pkglist = NULL ;
HyPackage pkg ;
guint i ;
gs_unref_ptrarray GPtrArray * nevras = g_ptr_array_new_with_free_func ( g_free ) ;
pkglist = hy_goal_list_installs ( goal ) ;
FOR_PACKAGELIST ( pkg , pkglist , i )
{
g_ptr_array_add ( nevras , hy_package_get_nevra ( pkg ) ) ;
}
g_ptr_array_sort ( nevras , ptrarray_sort_compare_strings ) ;
for ( i = 0 ; i < nevras - > len ; i + + )
{
const char * nevra = nevras - > pdata [ i ] ;
g_checksum_update ( checksum , ( guint8 * ) nevra , strlen ( nevra ) ) ;
}
}
ret_checksum = g_strdup ( g_checksum_get_string ( checksum ) ) ;
ret = TRUE ;
gs_transfer_out_value ( out_checksum , & ret_checksum ) ;
if ( checksum ) g_checksum_free ( checksum ) ;
return ret ;
}
2015-01-02 01:32:40 +03:00
static void
on_hifstate_percentage_changed ( HifState * hifstate ,
guint percentage ,
gpointer user_data )
{
const char * text = user_data ;
2015-02-18 00:54:54 +03:00
glnx_console_progress_text_percent ( text , percentage ) ;
2015-01-02 01:32:40 +03:00
}
2014-02-13 03:26:31 +04:00
static gboolean
2015-01-01 02:18:33 +03:00
install_packages_in_root ( RpmOstreeTreeComposeContext * self ,
JsonObject * treedata ,
GFile * yumroot ,
char * * packages ,
2015-01-03 05:54:23 +03:00
gboolean * out_unmodified ,
char * * out_new_inputhash ,
2015-01-01 02:18:33 +03:00
GCancellable * cancellable ,
GError * * error )
2014-01-24 22:34:19 +04:00
{
2014-02-13 03:26:31 +04:00
gboolean ret = FALSE ;
2015-01-02 01:32:40 +03:00
guint progress_sigid ;
2015-01-01 02:18:33 +03:00
char * * strviter ;
GFile * contextdir = self - > treefile_context_dirs - > pdata [ 0 ] ;
gs_unref_object HifContext * hifctx = NULL ;
gs_free char * cachedir = g_build_filename ( gs_file_get_path_cached ( self - > workdir ) ,
" cache " ,
NULL ) ;
gs_free char * solvdir = g_build_filename ( gs_file_get_path_cached ( self - > workdir ) ,
" solv " ,
NULL ) ;
gs_free char * lockdir = g_build_filename ( gs_file_get_path_cached ( self - > workdir ) ,
" lock " ,
NULL ) ;
2015-01-03 05:54:23 +03:00
gs_free char * ret_new_inputhash = NULL ;
2015-01-01 02:18:33 +03:00
2015-01-02 01:44:57 +03:00
/* Apparently there's only one process-global macro context;
* realistically , we ' re going to have to refactor all of the RPM
* stuff to a subprocess .
*/
2015-01-01 02:18:33 +03:00
hifctx = hif_context_new ( ) ;
2015-01-04 06:07:22 +03:00
hif_context_set_http_proxy ( hifctx , g_getenv ( " http_proxy " ) ) ;
2015-01-01 02:18:33 +03:00
hif_context_set_install_root ( hifctx , gs_file_get_path_cached ( yumroot ) ) ;
hif_context_set_cache_dir ( hifctx , cachedir ) ;
hif_context_set_solv_dir ( hifctx , solvdir ) ;
hif_context_set_lock_dir ( hifctx , lockdir ) ;
hif_context_set_check_disk_space ( hifctx , FALSE ) ;
hif_context_set_check_transaction ( hifctx , FALSE ) ;
hif_context_set_repo_dir ( hifctx , gs_file_get_path_cached ( contextdir ) ) ;
2015-01-02 01:44:57 +03:00
{ JsonNode * install_langs_n =
json_object_get_member ( treedata , " install-langs " ) ;
if ( install_langs_n ! = NULL )
{
JsonArray * instlangs_a = json_node_get_array ( install_langs_n ) ;
guint len = json_array_get_length ( instlangs_a ) ;
guint i ;
GString * opt = g_string_new ( " " ) ;
for ( i = 0 ; i < len ; i + + )
{
g_string_append ( opt , json_array_get_string_element ( instlangs_a , i ) ) ;
if ( i < len - 1 )
g_string_append_c ( opt , ' : ' ) ;
}
hif_context_set_rpm_macro ( hifctx , " _install_langs " , opt - > str ) ;
g_string_free ( opt , TRUE ) ;
}
}
2015-01-01 02:18:33 +03:00
if ( ! hif_context_setup ( hifctx , cancellable , error ) )
2014-02-26 02:07:59 +04:00
goto out ;
2014-01-26 19:13:45 +04:00
2015-01-02 05:13:17 +03:00
/* Forcibly override rpm/librepo SIGINT handlers. We always operate
* in a fully idempotent / atomic mode , and can be killed at any time .
*/
signal ( SIGINT , SIG_DFL ) ;
signal ( SIGTERM , SIG_DFL ) ;
2015-01-01 02:18:33 +03:00
/* Bind the json \"repos\" member to the hif state, which looks at the
* enabled = member of the repos file . By default we forcibly enable
* only repos which are specified , ignoring the enabled = flag .
*/
2014-05-18 17:29:51 +04:00
{
2015-01-01 02:18:33 +03:00
GPtrArray * sources ;
JsonArray * enable_repos = NULL ;
gs_unref_hashtable GHashTable * enabled_repo_names =
g_hash_table_new ( g_str_hash , g_str_equal ) ;
guint i ;
guint n ;
2014-07-11 22:02:45 +04:00
2015-01-01 02:18:33 +03:00
sources = hif_context_get_sources ( hifctx ) ;
2014-07-11 22:02:45 +04:00
2015-01-01 02:18:33 +03:00
if ( ! json_object_has_member ( treedata , " repos " ) )
2014-05-18 22:13:31 +04:00
{
2015-01-01 02:18:33 +03:00
g_set_error_literal ( error , G_IO_ERROR , G_IO_ERROR_FAILED ,
" Treefile is missing required \" repos \" member " ) ;
goto out ;
2014-05-18 22:13:31 +04:00
}
2014-02-26 02:07:59 +04:00
enable_repos = json_object_get_array_member ( treedata , " repos " ) ;
2015-01-01 02:18:33 +03:00
n = json_array_get_length ( enable_repos ) ;
2014-02-13 03:26:31 +04:00
2015-01-01 02:18:33 +03:00
for ( i = 0 ; i < n ; i + + )
{
const char * reponame = _rpmostree_jsonutil_array_require_string_element ( enable_repos , i , error ) ;
if ( ! reponame )
goto out ;
g_hash_table_add ( enabled_repo_names , ( char * ) reponame ) ;
}
2014-05-05 16:34:45 +04:00
2015-01-01 02:18:33 +03:00
for ( i = 0 ; i < sources - > len ; i + + )
{
HifSource * src = g_ptr_array_index ( sources , i ) ;
2014-05-05 16:34:45 +04:00
2015-01-01 02:18:33 +03:00
if ( ! g_hash_table_lookup ( enabled_repo_names , hif_source_get_id ( src ) ) )
hif_source_set_enabled ( src , HIF_SOURCE_ENABLED_NONE ) ;
else
hif_source_set_enabled ( src , HIF_SOURCE_ENABLED_PACKAGES ) ;
}
2014-02-26 02:07:59 +04:00
}
2014-01-26 19:13:45 +04:00
2015-01-17 01:23:17 +03:00
{ gboolean docs = TRUE ;
if ( ! _rpmostree_jsonutil_object_get_optional_boolean_member ( treedata ,
" documentation " ,
& docs ,
error ) )
goto out ;
if ( ! docs )
hif_transaction_set_flags ( hif_context_get_transaction ( hifctx ) ,
HIF_TRANSACTION_FLAG_NODOCS ) ;
}
2015-01-02 01:32:40 +03:00
/* --- Downloading metadata --- */
2015-02-18 00:54:54 +03:00
{ g_auto ( GLnxConsoleRef ) console = { 0 , } ;
2015-01-02 01:32:40 +03:00
gs_unref_object HifState * hifstate = hif_state_new ( ) ;
progress_sigid = g_signal_connect ( hifstate , " percentage-changed " ,
G_CALLBACK ( on_hifstate_percentage_changed ) ,
" Downloading metadata: " ) ;
2015-02-18 00:54:54 +03:00
glnx_console_lock ( & console ) ;
2015-01-02 01:32:40 +03:00
if ( ! hif_context_setup_sack ( hifctx , hifstate , error ) )
goto out ;
g_signal_handler_disconnect ( hifstate , progress_sigid ) ;
}
2014-01-24 22:34:19 +04:00
for ( strviter = packages ; strviter & & * strviter ; strviter + + )
{
2015-01-01 02:18:33 +03:00
if ( ! hif_context_install ( hifctx , * strviter , error ) )
2014-01-26 19:13:45 +04:00
goto out ;
2014-01-24 22:34:19 +04:00
}
2015-01-02 01:32:40 +03:00
/* --- Resolving dependencies --- */
2015-02-18 00:54:54 +03:00
{ g_auto ( GLnxConsoleRef ) console = { 0 , } ;
2015-01-02 01:32:40 +03:00
gs_unref_object HifState * hifstate = hif_state_new ( ) ;
progress_sigid = g_signal_connect ( hifstate , " percentage-changed " ,
G_CALLBACK ( on_hifstate_percentage_changed ) ,
" Resolving dependencies: " ) ;
2015-02-18 00:54:54 +03:00
glnx_console_lock ( & console ) ;
2015-01-02 01:32:40 +03:00
if ( ! hif_transaction_depsolve ( hif_context_get_transaction ( hifctx ) ,
hif_context_get_goal ( hifctx ) ,
hifstate , error ) )
goto out ;
2014-01-24 22:34:19 +04:00
2015-01-02 01:32:40 +03:00
g_signal_handler_disconnect ( hifstate , progress_sigid ) ;
}
2015-01-03 05:54:23 +03:00
if ( ! compute_checksum_from_treefile_and_goal ( self , hif_context_get_goal ( hifctx ) ,
& ret_new_inputhash , error ) )
goto out ;
2015-01-08 05:40:32 +03:00
/* Only look for previous checksum if caller has passed *out_unmodified */
if ( self - > previous_checksum & & out_unmodified ! = NULL )
2015-01-03 05:54:23 +03:00
{
gs_unref_variant GVariant * commit_v = NULL ;
gs_unref_variant GVariant * commit_metadata = NULL ;
const char * previous_inputhash = NULL ;
if ( ! ostree_repo_load_variant ( self - > repo , OSTREE_OBJECT_TYPE_COMMIT ,
self - > previous_checksum ,
& commit_v , error ) )
goto out ;
commit_metadata = g_variant_get_child_value ( commit_v , 0 ) ;
if ( g_variant_lookup ( commit_metadata , " rpmostree.inputhash " , " &s " , & previous_inputhash ) )
{
if ( strcmp ( previous_inputhash , ret_new_inputhash ) = = 0 )
{
* out_unmodified = TRUE ;
ret = TRUE ;
goto out ;
}
}
else
g_print ( " Previous commit found, but without rpmostree.inputhash metadata key \n " ) ;
}
2015-01-02 01:32:40 +03:00
/* --- Downloading packages --- */
2015-02-18 00:54:54 +03:00
{ g_auto ( GLnxConsoleRef ) console = { 0 , } ;
2015-01-02 01:32:40 +03:00
gs_unref_object HifState * hifstate = hif_state_new ( ) ;
progress_sigid = g_signal_connect ( hifstate , " percentage-changed " ,
G_CALLBACK ( on_hifstate_percentage_changed ) ,
" Downloading packages: " ) ;
2015-02-18 00:54:54 +03:00
glnx_console_lock ( & console ) ;
2015-01-02 01:32:40 +03:00
if ( ! hif_transaction_download ( hif_context_get_transaction ( hifctx ) , hifstate , error ) )
goto out ;
g_signal_handler_disconnect ( hifstate , progress_sigid ) ;
}
2015-02-18 00:54:54 +03:00
{ g_auto ( GLnxConsoleRef ) console = { 0 , } ;
2015-01-02 01:32:40 +03:00
gs_unref_object HifState * hifstate = hif_state_new ( ) ;
progress_sigid = g_signal_connect ( hifstate , " percentage-changed " ,
G_CALLBACK ( on_hifstate_percentage_changed ) ,
" Installing packages: " ) ;
2015-02-18 00:54:54 +03:00
glnx_console_lock ( & console ) ;
2015-01-02 01:32:40 +03:00
if ( ! hif_transaction_commit ( hif_context_get_transaction ( hifctx ) ,
hif_context_get_goal ( hifctx ) ,
hifstate ,
error ) )
goto out ;
g_signal_handler_disconnect ( hifstate , progress_sigid ) ;
}
2014-01-24 22:34:19 +04:00
ret = TRUE ;
2015-01-08 05:40:32 +03:00
if ( out_unmodified )
* out_unmodified = FALSE ;
2015-01-03 05:54:23 +03:00
gs_transfer_out_value ( out_new_inputhash , & ret_new_inputhash ) ;
2014-01-24 22:34:19 +04:00
out :
return ret ;
}
2014-05-03 18:05:43 +04:00
static gboolean
2014-05-18 22:13:31 +04:00
process_includes ( RpmOstreeTreeComposeContext * self ,
GFile * treefile_path ,
2014-05-03 18:05:43 +04:00
guint depth ,
JsonObject * root ,
GCancellable * cancellable ,
GError * * error )
{
gboolean ret = FALSE ;
const char * include_path ;
const guint maxdepth = 50 ;
if ( depth > maxdepth )
{
g_set_error ( error , G_IO_ERROR , G_IO_ERROR_FAILED ,
" Exceeded maximum include depth of %u " , maxdepth ) ;
goto out ;
}
2014-05-18 22:13:31 +04:00
{
gs_unref_object GFile * parent = g_file_get_parent ( treefile_path ) ;
gboolean existed = FALSE ;
if ( self - > treefile_context_dirs - > len > 0 )
{
GFile * prev = self - > treefile_context_dirs - > pdata [ self - > treefile_context_dirs - > len - 1 ] ;
if ( g_file_equal ( parent , prev ) )
existed = TRUE ;
}
if ( ! existed )
{
g_ptr_array_add ( self - > treefile_context_dirs , parent ) ;
parent = NULL ; /* Transfer ownership */
}
}
2014-11-13 22:53:43 +03:00
if ( ! _rpmostree_jsonutil_object_get_optional_string_member ( root , " include " , & include_path , error ) )
2014-05-03 18:05:43 +04:00
goto out ;
if ( include_path )
{
gs_unref_object GFile * treefile_dirpath = g_file_get_parent ( treefile_path ) ;
gs_unref_object GFile * parent_path = g_file_resolve_relative_path ( treefile_dirpath , include_path ) ;
gs_unref_object JsonParser * parent_parser = json_parser_new ( ) ;
JsonNode * parent_rootval ;
JsonObject * parent_root ;
GList * members ;
GList * iter ;
if ( ! json_parser_load_from_file ( parent_parser ,
gs_file_get_path_cached ( parent_path ) ,
error ) )
goto out ;
parent_rootval = json_parser_get_root ( parent_parser ) ;
if ( ! JSON_NODE_HOLDS_OBJECT ( parent_rootval ) )
{
g_set_error ( error , G_IO_ERROR , G_IO_ERROR_FAILED ,
" Treefile root is not an object " ) ;
goto out ;
}
parent_root = json_node_get_object ( parent_rootval ) ;
2014-05-18 22:13:31 +04:00
if ( ! process_includes ( self , parent_path , depth + 1 , parent_root ,
2014-05-03 18:05:43 +04:00
cancellable , error ) )
goto out ;
members = json_object_get_members ( parent_root ) ;
for ( iter = members ; iter ; iter = iter - > next )
{
const char * name = iter - > data ;
JsonNode * parent_val = json_object_get_member ( parent_root , name ) ;
JsonNode * val = json_object_get_member ( root , name ) ;
g_assert ( parent_val ) ;
if ( ! val )
json_object_set_member ( root , name , json_node_copy ( parent_val ) ) ;
else
{
JsonNodeType parent_type =
json_node_get_node_type ( parent_val ) ;
JsonNodeType child_type =
json_node_get_node_type ( val ) ;
if ( parent_type ! = child_type )
{
g_set_error ( error , G_IO_ERROR , G_IO_ERROR_FAILED ,
" Conflicting element type of '%s' " ,
name ) ;
goto out ;
}
if ( child_type = = JSON_NODE_ARRAY )
{
JsonArray * parent_array = json_node_get_array ( parent_val ) ;
JsonArray * child_array = json_node_get_array ( val ) ;
JsonArray * new_child = json_array_new ( ) ;
guint i , len ;
len = json_array_get_length ( parent_array ) ;
for ( i = 0 ; i < len ; i + + )
json_array_add_element ( new_child , json_node_copy ( json_array_get_element ( parent_array , i ) ) ) ;
len = json_array_get_length ( child_array ) ;
for ( i = 0 ; i < len ; i + + )
json_array_add_element ( new_child , json_node_copy ( json_array_get_element ( child_array , i ) ) ) ;
json_object_set_array_member ( root , name , new_child ) ;
}
}
}
2014-09-07 20:38:34 +04:00
json_object_remove_member ( root , " include " ) ;
2014-05-03 18:05:43 +04:00
}
ret = TRUE ;
out :
return ret ;
}
2014-10-14 17:32:29 +04:00
static gboolean
parse_keyvalue_strings ( char * * strings ,
2015-01-03 05:21:30 +03:00
GVariantBuilder * builder ,
2014-10-14 17:32:29 +04:00
GError * * error )
{
gboolean ret = FALSE ;
char * * iter ;
for ( iter = strings ; * iter ; iter + + )
{
const char * s ;
const char * eq ;
gs_free char * key = NULL ;
s = * iter ;
eq = strchr ( s , ' = ' ) ;
if ( ! eq )
{
g_set_error ( error , G_IO_ERROR , G_IO_ERROR_FAILED ,
" Missing '=' in KEY=VALUE metadata '%s' " , s ) ;
goto out ;
}
key = g_strndup ( s , eq - s ) ;
g_variant_builder_add ( builder , " {sv} " , key ,
g_variant_new_string ( eq + 1 ) ) ;
}
ret = TRUE ;
out :
return ret ;
}
2015-01-07 09:30:27 +03:00
static gboolean
compose_strv_contains_prefix ( gchar * * strv ,
const gchar * prefix )
{
if ( ! strv )
return FALSE ;
while ( * strv )
{
if ( g_str_has_prefix ( * strv , prefix ) )
return TRUE ;
+ + strv ;
}
return FALSE ;
}
2014-03-22 23:05:41 +04:00
gboolean
2014-05-26 23:05:08 +04:00
rpmostree_compose_builtin_tree ( int argc ,
char * * argv ,
GCancellable * cancellable ,
GError * * error )
2014-01-24 22:34:19 +04:00
{
2014-03-22 23:05:41 +04:00
gboolean ret = FALSE ;
2015-01-03 04:42:25 +03:00
GError * temp_error = NULL ;
2014-01-24 22:34:19 +04:00
GOptionContext * context = g_option_context_new ( " - Run yum and commit the result to an OSTree repository " ) ;
const char * ref ;
2014-05-18 22:13:31 +04:00
RpmOstreeTreeComposeContext selfdata = { NULL , } ;
RpmOstreeTreeComposeContext * self = & selfdata ;
2014-05-03 18:05:43 +04:00
JsonNode * treefile_rootval = NULL ;
2014-02-13 03:26:31 +04:00
JsonObject * treefile = NULL ;
2014-05-16 01:46:51 +04:00
gs_free char * cachekey = NULL ;
2015-01-03 05:54:23 +03:00
gs_free char * new_inputhash = NULL ;
2014-01-24 22:34:19 +04:00
gs_unref_object GFile * cachedir = NULL ;
2015-01-03 04:42:25 +03:00
gs_unref_object GFile * previous_root = NULL ;
gs_free char * previous_checksum = NULL ;
2014-01-24 22:34:19 +04:00
gs_unref_object GFile * yumroot = NULL ;
gs_unref_object GFile * yumroot_varcache = NULL ;
2014-01-29 23:37:44 +04:00
gs_unref_object OstreeRepo * repo = NULL ;
2014-02-13 03:26:31 +04:00
gs_unref_ptrarray GPtrArray * bootstrap_packages = NULL ;
gs_unref_ptrarray GPtrArray * packages = NULL ;
gs_unref_object GFile * treefile_path = NULL ;
2015-01-20 09:37:22 +03:00
gs_unref_object GFile * treefile_dirpath = NULL ;
2014-05-03 14:55:35 +04:00
gs_unref_object GFile * repo_path = NULL ;
2014-02-13 03:26:31 +04:00
gs_unref_object JsonParser * treefile_parser = NULL ;
2015-01-03 05:21:30 +03:00
gs_unref_variant_builder GVariantBuilder * metadata_builder =
g_variant_builder_new ( G_VARIANT_TYPE ( " a{sv} " ) ) ;
2014-05-03 14:55:35 +04:00
gboolean workdir_is_tmp = FALSE ;
2014-05-18 22:13:31 +04:00
self - > treefile_context_dirs = g_ptr_array_new_with_free_func ( ( GDestroyNotify ) g_object_unref ) ;
2014-01-24 22:34:19 +04:00
2014-11-24 20:34:45 +03:00
if ( ! rpmostree_option_context_parse ( context , option_entries , & argc , & argv , error ) )
2014-01-24 22:34:19 +04:00
goto out ;
2014-03-22 23:05:41 +04:00
if ( argc < 2 )
2014-01-24 22:34:19 +04:00
{
2014-05-26 23:05:08 +04:00
g_printerr ( " usage: rpm-ostree compose tree TREEFILE \n " ) ;
2014-01-24 22:34:19 +04:00
g_set_error ( error , G_IO_ERROR , G_IO_ERROR_FAILED ,
" Option processing failed " ) ;
goto out ;
}
2014-05-03 14:55:35 +04:00
if ( ! opt_repo )
{
g_set_error ( error , G_IO_ERROR , G_IO_ERROR_FAILED ,
" --repo must be specified " ) ;
goto out ;
}
2014-11-10 04:28:10 +03:00
/* Use a private mount namespace to avoid polluting the global
* namespace , and to ensure any tmpfs mounts get cleaned up if we
* exit unexpectedly .
*
* We also rely on this for the yum confinement .
*/
if ( unshare ( CLONE_NEWNS ) ! = 0 )
{
2014-11-20 02:32:08 +03:00
_rpmostree_set_prefix_error_from_errno ( error , errno , " unshare(CLONE_NEWNS): " ) ;
2014-11-10 04:28:10 +03:00
goto out ;
}
if ( mount ( NULL , " / " , " none " , MS_PRIVATE | MS_REC , NULL ) = = - 1 )
{
compose: Introduce a little 'libcontainer', use it for the post script
The current motivation for this is that
https://github.com/fedora-infra/fedmsg-atomic-composer
started using mock --new-chroot (which uses systemd-nspawn) to run
rpm-ostree, which in turn uses systemd-nspawn to run the post script.
Now systemd-nspawn is not really nestable (it wants to link up
journald, resolv.conf handling, etc).
First, dropping nspawn and going to raw containers fixes the nesting
problem.
Second, we don't need all the features of systemd-nspawn. We are ok
with log messages going to stdout, and we don't use networking, so no
resolv.conf is needed.
Third, this sets a bit of a stage for more sandboxing internally when
run on real systems. I already have a prototype branch which runs
librepo as an unprivileged user, that could be combined with this for
even stronger security.
Why not use systemd? Well...I'm still debating that. But the core
problem is systemd isn't a library in the C sense - to use its
sandboxing features we have to use unit files. It's harder to have a
daemon that looks like a single service from a management perspective,
but uses sandboxing internally.
2014-11-20 02:30:00 +03:00
/* This happens on RHEL6, not going to debug it further right now... */
if ( errno = = EINVAL )
_rpmostree_libcontainer_set_not_available ( ) ;
else
{
_rpmostree_set_prefix_error_from_errno ( error , errno , " mount(/, MS_PRIVATE): " ) ;
goto out ;
}
2014-11-10 04:28:10 +03:00
}
/* Mount several directories read only for protection from librpm
* and any stray code in yum / hawkey .
*/
compose: Introduce a little 'libcontainer', use it for the post script
The current motivation for this is that
https://github.com/fedora-infra/fedmsg-atomic-composer
started using mock --new-chroot (which uses systemd-nspawn) to run
rpm-ostree, which in turn uses systemd-nspawn to run the post script.
Now systemd-nspawn is not really nestable (it wants to link up
journald, resolv.conf handling, etc).
First, dropping nspawn and going to raw containers fixes the nesting
problem.
Second, we don't need all the features of systemd-nspawn. We are ok
with log messages going to stdout, and we don't use networking, so no
resolv.conf is needed.
Third, this sets a bit of a stage for more sandboxing internally when
run on real systems. I already have a prototype branch which runs
librepo as an unprivileged user, that could be combined with this for
even stronger security.
Why not use systemd? Well...I'm still debating that. But the core
problem is systemd isn't a library in the C sense - to use its
sandboxing features we have to use unit files. It's harder to have a
daemon that looks like a single service from a management perspective,
but uses sandboxing internally.
2014-11-20 02:30:00 +03:00
if ( _rpmostree_libcontainer_get_available ( ) )
{
struct stat stbuf ;
/* Protect /var/lib/rpm if (and only if) it's a regular directory.
This happens when you ' re running compose - tree from inside a
" mainline " system . On an rpm - ostree based system ,
/ var / lib / rpm - > / usr / share / rpm , which is already protected by a read - only
bind mount . */
if ( lstat ( " /var/lib/rpm " , & stbuf ) = = 0 & & S_ISDIR ( stbuf . st_mode ) )
{
if ( ! _rpmostree_libcontainer_bind_mount_readonly ( " /var/lib/rpm " , error ) )
goto out ;
}
2014-11-10 04:28:10 +03:00
compose: Introduce a little 'libcontainer', use it for the post script
The current motivation for this is that
https://github.com/fedora-infra/fedmsg-atomic-composer
started using mock --new-chroot (which uses systemd-nspawn) to run
rpm-ostree, which in turn uses systemd-nspawn to run the post script.
Now systemd-nspawn is not really nestable (it wants to link up
journald, resolv.conf handling, etc).
First, dropping nspawn and going to raw containers fixes the nesting
problem.
Second, we don't need all the features of systemd-nspawn. We are ok
with log messages going to stdout, and we don't use networking, so no
resolv.conf is needed.
Third, this sets a bit of a stage for more sandboxing internally when
run on real systems. I already have a prototype branch which runs
librepo as an unprivileged user, that could be combined with this for
even stronger security.
Why not use systemd? Well...I'm still debating that. But the core
problem is systemd isn't a library in the C sense - to use its
sandboxing features we have to use unit files. It's harder to have a
daemon that looks like a single service from a management perspective,
but uses sandboxing internally.
2014-11-20 02:30:00 +03:00
/* Protect the system's /etc and /usr */
if ( ! _rpmostree_libcontainer_bind_mount_readonly ( " /etc " , error ) )
goto out ;
if ( ! _rpmostree_libcontainer_bind_mount_readonly ( " /usr " , error ) )
goto out ;
}
2014-11-10 04:28:10 +03:00
2014-05-03 14:55:35 +04:00
repo_path = g_file_new_for_path ( opt_repo ) ;
2015-01-03 05:54:23 +03:00
repo = self - > repo = ostree_repo_new ( repo_path ) ;
2014-05-03 14:55:35 +04:00
if ( ! ostree_repo_open ( repo , cancellable , error ) )
goto out ;
2014-10-14 17:32:29 +04:00
2014-03-22 23:05:41 +04:00
treefile_path = g_file_new_for_path ( argv [ 1 ] ) ;
2014-01-24 22:34:19 +04:00
2014-05-03 14:55:35 +04:00
if ( opt_workdir )
{
2014-11-17 04:05:47 +03:00
self - > workdir = g_file_new_for_path ( opt_workdir ) ;
2014-05-03 14:55:35 +04:00
}
else
{
gs_free char * tmpd = g_mkdtemp ( g_strdup ( " /var/tmp/rpm-ostree.XXXXXX " ) ) ;
2014-11-17 04:05:47 +03:00
self - > workdir = g_file_new_for_path ( tmpd ) ;
2014-05-03 14:55:35 +04:00
workdir_is_tmp = TRUE ;
2014-07-02 06:07:56 +04:00
if ( opt_workdir_tmpfs )
{
if ( mount ( " tmpfs " , tmpd , " tmpfs " , 0 , ( const void * ) " mode=755 " ) ! = 0 )
{
2014-11-20 02:32:08 +03:00
_rpmostree_set_prefix_error_from_errno ( error , errno ,
" mount(tmpfs): " ) ;
2014-07-02 06:07:56 +04:00
goto out ;
}
}
2014-05-03 14:55:35 +04:00
}
2015-02-15 21:56:14 +03:00
if ( ! glnx_opendirat ( AT_FDCWD , gs_file_get_path_cached ( self - > workdir ) ,
FALSE , & self - > workdir_dfd , error ) )
goto out ;
2014-05-16 01:46:51 +04:00
if ( opt_cachedir )
2014-06-07 02:25:08 +04:00
{
cachedir = g_file_new_for_path ( opt_cachedir ) ;
if ( ! gs_file_ensure_directory ( cachedir , FALSE , cancellable , error ) )
goto out ;
}
2014-05-16 01:46:51 +04:00
2014-10-14 17:32:29 +04:00
if ( opt_metadata_strings )
{
if ( ! parse_keyvalue_strings ( opt_metadata_strings ,
2015-01-03 05:21:30 +03:00
metadata_builder , error ) )
2014-10-14 17:32:29 +04:00
goto out ;
}
2015-02-15 21:56:14 +03:00
if ( fchdir ( self - > workdir_dfd ) ! = 0 )
2014-01-24 22:34:19 +04:00
{
2015-02-15 21:56:14 +03:00
glnx_set_error_from_errno ( error ) ;
2014-02-13 03:26:31 +04:00
goto out ;
2014-01-24 22:34:19 +04:00
}
2014-02-13 03:26:31 +04:00
treefile_parser = json_parser_new ( ) ;
if ( ! json_parser_load_from_file ( treefile_parser ,
gs_file_get_path_cached ( treefile_path ) ,
error ) )
goto out ;
2014-05-03 18:05:43 +04:00
treefile_rootval = json_parser_get_root ( treefile_parser ) ;
if ( ! JSON_NODE_HOLDS_OBJECT ( treefile_rootval ) )
2014-02-13 03:26:31 +04:00
{
g_set_error ( error , G_IO_ERROR , G_IO_ERROR_FAILED ,
" Treefile root is not an object " ) ;
goto out ;
}
2014-05-03 18:05:43 +04:00
treefile = json_node_get_object ( treefile_rootval ) ;
2014-05-18 22:13:31 +04:00
if ( ! process_includes ( self , treefile_path , 0 , treefile ,
2014-05-03 18:05:43 +04:00
cancellable , error ) )
goto out ;
if ( opt_print_only )
{
gs_unref_object JsonGenerator * generator = json_generator_new ( ) ;
gs_unref_object GOutputStream * stdout = g_unix_output_stream_new ( 1 , FALSE ) ;
json_generator_set_pretty ( generator , TRUE ) ;
json_generator_set_root ( generator , treefile_rootval ) ;
( void ) json_generator_to_stream ( generator , stdout , NULL , NULL ) ;
ret = TRUE ;
goto out ;
}
2014-02-13 03:26:31 +04:00
2014-11-13 22:53:43 +03:00
ref = _rpmostree_jsonutil_object_require_string_member ( treefile , " ref " , error ) ;
2014-02-13 03:26:31 +04:00
if ( ! ref )
goto out ;
2015-01-03 04:42:25 +03:00
if ( ! ostree_repo_read_commit ( repo , ref , & previous_root , & previous_checksum ,
cancellable , & temp_error ) )
{
if ( g_error_matches ( temp_error , G_IO_ERROR , G_IO_ERROR_NOT_FOUND ) )
{
g_clear_error ( & temp_error ) ;
g_print ( " No previous commit for %s \n " , ref ) ;
}
else
{
g_propagate_error ( error , temp_error ) ;
goto out ;
}
}
else
g_print ( " Previous commit: %s \n " , previous_checksum ) ;
2015-01-03 05:54:23 +03:00
self - > previous_checksum = previous_checksum ;
2015-01-03 04:42:25 +03:00
yumroot = g_file_get_child ( self - > workdir , " rootfs.tmp " ) ;
2015-02-15 21:56:14 +03:00
if ( ! glnx_shutil_rm_rf_at ( self - > workdir_dfd , " rootfs.tmp " , cancellable , error ) )
2015-01-03 04:42:25 +03:00
goto out ;
2015-01-07 09:30:27 +03:00
if ( json_object_has_member ( treefile , " automatic_version_prefix " ) & &
! compose_strv_contains_prefix ( opt_metadata_strings , " version= " ) )
{
gs_unref_variant GVariant * variant = NULL ;
gs_free char * last_version = NULL ;
gs_free char * next_version = NULL ;
const char * ver_prefix ;
ver_prefix = _rpmostree_jsonutil_object_require_string_member ( treefile ,
" automatic_version_prefix " ,
error ) ;
if ( ! ver_prefix )
goto out ;
if ( previous_checksum )
{
if ( ! ostree_repo_load_variant ( repo , OSTREE_OBJECT_TYPE_COMMIT ,
previous_checksum , & variant , error ) )
goto out ;
last_version = checksum_version ( variant ) ;
}
2015-01-12 08:07:33 +03:00
next_version = _rpmostree_util_next_version ( ver_prefix , last_version ) ;
2015-01-07 09:30:27 +03:00
g_variant_builder_add ( metadata_builder , " {sv} " , " version " ,
g_variant_new_string ( next_version ) ) ;
}
2014-02-13 03:26:31 +04:00
bootstrap_packages = g_ptr_array_new ( ) ;
packages = g_ptr_array_new ( ) ;
2014-01-24 22:34:19 +04:00
2014-11-17 23:48:08 +03:00
if ( json_object_has_member ( treefile , " bootstrap_packages " ) )
{
2014-11-18 00:08:03 +03:00
if ( ! _rpmostree_jsonutil_append_string_array_to ( treefile , " bootstrap_packages " , packages , error ) )
2014-11-17 23:48:08 +03:00
goto out ;
}
2014-11-18 00:08:03 +03:00
if ( ! _rpmostree_jsonutil_append_string_array_to ( treefile , " packages " , packages , error ) )
2014-02-13 03:26:31 +04:00
goto out ;
g_ptr_array_add ( packages , NULL ) ;
2014-01-24 22:34:19 +04:00
2014-09-30 00:24:53 +04:00
{
gs_unref_object JsonGenerator * generator = json_generator_new ( ) ;
char * treefile_buf = NULL ;
gsize len ;
json_generator_set_root ( generator , treefile_rootval ) ;
json_generator_set_pretty ( generator , TRUE ) ;
treefile_buf = json_generator_to_data ( generator , & len ) ;
self - > serialized_treefile = g_bytes_new_take ( treefile_buf , len ) ;
}
2015-01-20 09:37:22 +03:00
treefile_dirpath = g_file_get_parent ( treefile_path ) ;
if ( TRUE )
2015-01-03 04:42:25 +03:00
{
gboolean generate_from_previous = TRUE ;
2014-12-24 00:28:53 +03:00
2015-01-03 04:42:25 +03:00
if ( ! _rpmostree_jsonutil_object_get_optional_boolean_member ( treefile ,
" preserve-passwd " ,
& generate_from_previous ,
error ) )
goto out ;
2014-12-24 00:28:53 +03:00
2015-01-03 04:42:25 +03:00
if ( generate_from_previous )
{
2015-01-20 09:37:22 +03:00
if ( ! rpmostree_generate_passwd_from_previous ( repo , yumroot ,
treefile_dirpath ,
previous_root , treefile ,
2015-01-03 04:42:25 +03:00
cancellable , error ) )
goto out ;
}
}
2014-12-24 00:28:53 +03:00
2015-01-03 05:54:23 +03:00
{ gboolean unmodified = FALSE ;
2014-05-16 01:47:47 +04:00
2015-01-03 05:54:23 +03:00
if ( ! install_packages_in_root ( self , treefile , yumroot ,
( char * * ) packages - > pdata ,
2015-01-08 05:40:32 +03:00
opt_force_nocache ? NULL : & unmodified ,
2015-01-03 05:54:23 +03:00
& new_inputhash ,
cancellable , error ) )
2014-05-16 01:46:51 +04:00
goto out ;
2015-01-03 05:54:23 +03:00
if ( unmodified )
{
g_print ( " No apparent changes since previous commit; use --force-nocache to override \n " ) ;
ret = TRUE ;
goto out ;
}
}
2014-01-26 19:13:45 +04:00
2014-05-16 01:47:47 +04:00
if ( g_strcmp0 ( g_getenv ( " RPM_OSTREE_BREAK " ) , " post-yum " ) = = 0 )
goto out ;
2014-01-26 19:13:45 +04:00
2014-11-14 19:53:21 +03:00
if ( ! rpmostree_treefile_postprocessing ( yumroot , self - > treefile_context_dirs - > pdata [ 0 ] ,
self - > serialized_treefile , treefile ,
2014-11-13 22:54:33 +03:00
cancellable , error ) )
goto out ;
2014-11-17 17:18:02 +03:00
if ( ! rpmostree_prepare_rootfs_for_commit ( yumroot , treefile , cancellable , error ) )
goto out ;
2014-11-20 17:40:35 +03:00
2015-01-20 09:37:22 +03:00
if ( ! rpmostree_check_passwd ( repo , yumroot , treefile_dirpath , treefile ,
2014-11-20 17:40:35 +03:00
cancellable , error ) )
goto out ;
2015-01-20 09:37:22 +03:00
if ( ! rpmostree_check_groups ( repo , yumroot , treefile_dirpath , treefile ,
2014-11-20 17:40:35 +03:00
cancellable , error ) )
goto out ;
2014-05-16 01:47:47 +04:00
{
const char * gpgkey ;
2015-01-03 05:54:23 +03:00
gs_unref_variant GVariant * metadata = NULL ;
g_variant_builder_add ( metadata_builder , " {sv} " ,
" rpmostree.inputhash " ,
g_variant_new_string ( new_inputhash ) ) ;
metadata = g_variant_ref_sink ( g_variant_builder_end ( metadata_builder ) ) ;
2015-01-03 05:21:30 +03:00
2014-11-13 22:53:43 +03:00
if ( ! _rpmostree_jsonutil_object_get_optional_string_member ( treefile , " gpg_key " , & gpgkey , error ) )
2014-05-16 01:47:47 +04:00
goto out ;
2014-03-29 04:15:43 +04:00
2014-10-14 17:32:29 +04:00
if ( ! rpmostree_commit ( yumroot , repo , ref , metadata , gpgkey ,
2014-05-16 01:47:47 +04:00
json_object_get_boolean_member ( treefile , " selinux " ) ,
cancellable , error ) )
2014-01-29 23:37:44 +04:00
goto out ;
2014-01-24 22:34:19 +04:00
}
2015-01-29 00:34:29 +03:00
if ( opt_touch_if_changed )
{
gs_fd_close int fd = open ( opt_touch_if_changed , O_CREAT | O_WRONLY | O_NOCTTY , 0644 ) ;
if ( fd = = - 1 )
{
gs_set_error_from_errno ( error , errno ) ;
g_prefix_error ( error , " Updating '%s': " , opt_touch_if_changed ) ;
goto out ;
}
if ( futimens ( fd , NULL ) = = - 1 )
{
gs_set_error_from_errno ( error , errno ) ;
goto out ;
}
}
2014-01-24 22:34:19 +04:00
out :
2014-07-02 06:07:56 +04:00
2014-06-11 15:27:31 +04:00
if ( workdir_is_tmp )
2014-07-02 06:07:56 +04:00
{
if ( opt_workdir_tmpfs )
2014-11-17 04:05:47 +03:00
( void ) umount ( gs_file_get_path_cached ( self - > workdir ) ) ;
( void ) gs_shutil_rm_rf ( self - > workdir , NULL , NULL ) ;
2014-07-02 06:07:56 +04:00
}
2014-05-18 22:13:31 +04:00
if ( self )
{
2014-11-17 04:05:47 +03:00
g_clear_object ( & self - > workdir ) ;
2014-09-07 20:38:34 +04:00
g_clear_pointer ( & self - > serialized_treefile , g_bytes_unref ) ;
2014-05-18 22:13:31 +04:00
g_ptr_array_unref ( self - > treefile_context_dirs ) ;
}
2014-03-22 23:05:41 +04:00
return ret ;
2014-01-24 22:34:19 +04:00
}