def test_load_config_external_app(self): """Test load config for external app""" params = [{ 'name': 'key1', 'description': 'key1' }, { 'name': 'key2', 'description': 'key2' }] initial_config = { 'general': { 'a': 'b', 'key2': 'val2' }, 'some-app': { 'key1': 'val1' } } config = Config(answers=initial_config) mock_nulecule = mock.Mock(name='nulecule', spec=Nulecule('some-id', '0.0.2', config, [], 'some/path')) nc = NuleculeComponent('some-app', 'some/path', params=params) nc._app = mock_nulecule nc.config = config nc.load_config() mock_nulecule.load_config.assert_called_once_with(config=config, ask=False, skip_asking=False)
def test_load_config_with_default_provider(self): """ Test Nulecule load_config with a default provider. """ config = Config(answers={}) params = [{ "name": "key1", "default": "val1", }, { "name": "key3", "default": "val3" }, { "name": "provider", "default": "docker" }] graph = [{ "name": "component1", "params": [{ "name": "key1", }, { "name": "key2", "default": "val2" }], "artifacts": [] }] n = Nulecule(id='some-id', specversion='0.0.2', metadata={}, graph=graph, params=params, basepath='some/path', config=config) n.load_components() n.load_config(config) self.assertEqual( n.config.runtime_answers(), { 'general': { 'namespace': 'default', 'provider': 'docker', 'key1': 'val1', 'key3': 'val3' }, 'component1': { 'key2': 'val2' } }) self.assertEqual( n.components[0].config.context(scope=n.components[0].namespace), { 'key3': 'val3', 'key2': 'val2', 'key1': 'val1', 'provider': 'docker', 'namespace': 'default' })
def test_get_provider_failure(self): """ Test if get_provider method when passed an invalid key raises an exception. """ nb = NuleculeBase(params=[], basepath='', namespace='') # purposefully give the wrong provider key provider_key = u'mesos' nb.config = Config(answers={u'general': {u'provider': provider_key}}) with self.assertRaises(NuleculeException): nb.get_provider()
def test_render_for_local_app_with_missing_artifacts_for_provider(self): """ Test rendering a Nulecule component with missing artifacts for a provider. """ provider_key = 'some-provider' dryrun = False nc = NuleculeComponent(name='some-app', basepath='some/path') nc.config = Config() nc.artifacts = {'x': ['some-artifact']} self.assertRaises(NuleculeException, nc.render, provider_key, dryrun)
def test_run(self): provider = 'docker' dryrun = False mock_component_1 = mock.Mock() mock_component_2 = mock.Mock() config = Config(answers={}) n = Nulecule('some-id', '0.0.2', [{}], 'some/path', {}, config=config) n.components = [mock_component_1, mock_component_2] n.run(provider) mock_component_1.run.assert_called_once_with(provider, dryrun) mock_component_2.run.assert_called_once_with(provider, dryrun)
def test_render_for_local_app_with_artifacts_for_provider( self, mock_render_artifact, mock_get_artifact_paths_for_provider): """Test rendering artifacts for a local Nulecule component""" provider_key = 'some-provider' dryrun = False expected_rendered_artifacts = [ 'some/path/.artifact1', 'some/path/.artifact2' ] mock_get_artifact_paths_for_provider.return_value = [ 'some/path/artifact1', 'some/path/artifact2' ] mock_render_artifact.side_effect = lambda path, context, provider: path.replace( 'artifact', '.artifact') # mock_get_context.return_value = context nc = NuleculeComponent(name='some-app', basepath='some/path') nc.config = Config(answers={ 'general': { 'key1': 'val1' }, 'some-provider': { 'a': 'b' } }) nc.artifacts = { 'some-provider': ['artifact1', 'artifact2'], 'x': ['foo'] } nc.render(provider_key, dryrun) expected_context = { 'key1': 'val1', 'namespace': 'default', 'provider': 'kubernetes' } mock_get_artifact_paths_for_provider.assert_called_once_with( provider_key) mock_render_artifact.assert_any_call('some/path/artifact1', expected_context, 'some-provider') mock_render_artifact.assert_any_call('some/path/artifact2', expected_context, 'some-provider') mock_get_artifact_paths_for_provider.assert_called_once_with( provider_key) self.assertEqual(nc.rendered_artifacts[provider_key], expected_rendered_artifacts)
def test_get_provider_success(self): """ Test if get_provider method when passed a particular valid key returns the corresponding class. """ nb = NuleculeBase(params=[], basepath='', namespace='') provider_key = u'openshift' # method `get_provider` will read from this config, we give it here # since we have neither provided it before nor it is auto-generated nb.config = Config(answers={u'general': {u'provider': provider_key}}) return_provider = mock.Mock() # mocking return value of method plugin.getProvider,because it returns # provider class and that class gets called with values nb.plugin.getProvider = mock.Mock(return_value=return_provider) ret_provider_key, ret_provider = nb.get_provider() self.assertEqual(provider_key, ret_provider_key) return_provider.assert_called_with( { 'provider': provider_key, 'namespace': 'default' }, '', False)
def test_load_config_local_app(self): """Test load config for local app""" params = [{ 'name': 'key1', 'description': 'key1' }, { 'name': 'key2', 'description': 'key2' }] initial_config = { 'general': { 'a': 'b', 'key2': 'val2' }, 'some-app': { 'key1': 'val1' } } conf = Config(answers=initial_config) nc = NuleculeComponent('some-app', 'some/path', params=params, config=conf) nc.load_config() runtime_answers = nc.config.runtime_answers() self.assertEqual( runtime_answers, { 'general': { 'a': 'b', 'key2': 'val2', 'provider': 'kubernetes', 'namespace': 'default' }, 'some-app': { 'key1': 'val1' } })
def test_load_components(self, MockNuleculeComponent): graph = [{ 'name': 'app1', 'source': 'docker://somecontainer', 'params': [] }, { 'name': 'app2', 'artifacts': [{ 'a': 'b' }] }] config = Config(answers={}) n = Nulecule('some-id', '0.0.2', graph, 'some/path', config=config) n.load_components() MockNuleculeComponent.assert_any_call(graph[0]['name'], n.basepath, 'somecontainer', graph[0]['params'], None, config) MockNuleculeComponent.assert_any_call(graph[1]['name'], n.basepath, None, graph[1].get('params'), graph[1].get('artifacts'), config)
def __init__(self, app_spec, destination=None, cli_answers=None, answers_file=None, answers_format=None): """ init function for NuleculeManager. Sets a few instance variables. Args: app_spec: either a path to an unpacked nulecule app or a container image name where a nulecule can be found destination: where to unpack a nulecule to if it isn't local cli_answers: some answer file values provided from cli args answers_file: the location of the answers file answers_format (str): File format for writing sample answers file """ self.answers_format = answers_format or ANSWERS_FILE_SAMPLE_FORMAT self.answers_file = None # The path to an answer file self.app_path = None # The path where the app resides or will reside self.image = None # The container image to pull the app from # Adjust app_spec, destination, and answer file paths if absolute. if os.path.isabs(app_spec): app_spec = Utils.get_real_abspath(app_spec) if destination and os.path.isabs(destination): destination = Utils.get_real_abspath(destination) if answers_file and os.path.isabs(answers_file): answers_file = Utils.get_real_abspath(answers_file) # If the user doesn't want the files copied to a permanent # location then he provides 'none'. If that is the case we'll # use a temporary directory if destination and destination.lower() == "none": logger.debug("'none' destination requested. Using tmp dir") destination = tempfile.mkdtemp(prefix="atomicapp") # Determine if the user passed us an image or a path to an app if not os.path.exists(app_spec): self.image = app_spec else: self.app_path = app_spec # Doesn't really make much sense to provide an app path and destination, # but if they want to we'll simply just copy the files for them if self.app_path and destination: Utils.copy_dir(self.app_path, destination, update=True) self.app_path = destination # If the user provided an image, make sure we have a destination if self.image: if destination: self.app_path = destination else: self.app_path = Utils.getNewAppCacheDir(self.image) logger.debug("NuleculeManager init app_path: %s", self.app_path) logger.debug("NuleculeManager init image: %s", self.image) # Create the app_path if it doesn't exist yet if not os.path.isdir(self.app_path): os.makedirs(self.app_path) # Set where the main nulecule file should be self.main_file = os.path.join(self.app_path, MAIN_FILE) # Process answers. self.answers_file = answers_file self.config = Config(cli=cli_answers)
class NuleculeManager(object): """ Interface to fetch, run, stop a Nulecule application. """ def __init__(self, app_spec, destination=None, cli_answers=None, answers_file=None, answers_format=None): """ init function for NuleculeManager. Sets a few instance variables. Args: app_spec: either a path to an unpacked nulecule app or a container image name where a nulecule can be found destination: where to unpack a nulecule to if it isn't local cli_answers: some answer file values provided from cli args answers_file: the location of the answers file answers_format (str): File format for writing sample answers file """ self.answers_format = answers_format or ANSWERS_FILE_SAMPLE_FORMAT self.answers_file = None # The path to an answer file self.app_path = None # The path where the app resides or will reside self.image = None # The container image to pull the app from # Adjust app_spec, destination, and answer file paths if absolute. if os.path.isabs(app_spec): app_spec = Utils.get_real_abspath(app_spec) if destination and os.path.isabs(destination): destination = Utils.get_real_abspath(destination) if answers_file and os.path.isabs(answers_file): answers_file = Utils.get_real_abspath(answers_file) # If the user doesn't want the files copied to a permanent # location then he provides 'none'. If that is the case we'll # use a temporary directory if destination and destination.lower() == "none": logger.debug("'none' destination requested. Using tmp dir") destination = tempfile.mkdtemp(prefix="atomicapp") # Determine if the user passed us an image or a path to an app if not os.path.exists(app_spec): self.image = app_spec else: self.app_path = app_spec # Doesn't really make much sense to provide an app path and destination, # but if they want to we'll simply just copy the files for them if self.app_path and destination: Utils.copy_dir(self.app_path, destination, update=True) self.app_path = destination # If the user provided an image, make sure we have a destination if self.image: if destination: self.app_path = destination else: self.app_path = Utils.getNewAppCacheDir(self.image) logger.debug("NuleculeManager init app_path: %s", self.app_path) logger.debug("NuleculeManager init image: %s", self.image) # Create the app_path if it doesn't exist yet if not os.path.isdir(self.app_path): os.makedirs(self.app_path) # Set where the main nulecule file should be self.main_file = os.path.join(self.app_path, MAIN_FILE) # Process answers. self.answers_file = answers_file self.config = Config(cli=cli_answers) @staticmethod def init(app_name, destination=None, app_version="1.0", app_desc="App description"): """Initialize a new Nulecule app Args: app_name (str): Application name destination (str): Destination path app_version (str): Application version app_desc (str): Application description Returns: destination (str) """ # context to render template files for Atomic App context = dict( app_name=app_name, app_version=app_version, app_desc=app_desc, atomicapp_version=__ATOMICAPPVERSION__, nulecule_spec_version=__NULECULESPECVERSION__, ) if destination is None: destination = os.path.join(".", app_name) # Check if destination directory exists and is not empty if os.path.exists(destination) and os.path.isdir(destination) and os.listdir(destination): value = raw_input("Destination directory is not empty! " "Do you still want to proceed? [Y]/n: ") value = value or "y" if value.lower() != "y": return # Exit out as the user has chosen not to proceed # Temporary working dir to render the templates tmpdir = tempfile.mkdtemp(prefix="nulecule-new-app-") template_dir = os.path.join(os.path.dirname(__file__), "external/templates/nulecule") try: # Copy template dir to temporary working directory and render templates distutils.dir_util.copy_tree(template_dir, tmpdir) for item in os.walk(tmpdir): parent_dir, dirs, files = item for filename in files: if not filename.endswith(".tpl"): continue templ_path = os.path.join(parent_dir, filename) if parent_dir.endswith("artifacts/docker") or parent_dir.endswith("artifacts/kubernetes"): file_path = os.path.join(parent_dir, "{}_{}".format(app_name, filename[:-4])) else: file_path = os.path.join(parent_dir, filename[:-4]) with open(templ_path) as f: s = f.read() t = Template(s) with open(file_path, "w") as f: f.write(t.safe_substitute(**context)) os.remove(templ_path) # Copy rendered templates to destination directory distutils.dir_util.copy_tree(tmpdir, destination, True) finally: # Remove temporary working directory distutils.dir_util.remove_tree(tmpdir) return destination def unpack(self, update=False, dryrun=False, nodeps=False, config=None): """ Unpacks a Nulecule application from a Nulecule image to a path or load a Nulecule that already exists locally. Args: update (bool): Update existing Nulecule application in app_path, if True dryrun (bool): Do not make any change to the host system nodeps (bool): Do not unpack external dependencies config (dict): Config data, if any, to use for unpacking Returns: A Nulecule instance. """ logger.debug("Request to unpack to %s to %s" % (self.image, self.app_path)) # If the user provided an image then unpack it and return the # resulting Nulecule. Else, load from existing path if self.image: return Nulecule.unpack( self.image, self.app_path, config=config, nodeps=nodeps, dryrun=dryrun, update=update ) else: return Nulecule.load_from_path(self.app_path, dryrun=dryrun, config=config) def genanswers(self, dryrun=False, **kwargs): """ Renders artifacts and then generates an answer file. Finally copies answer file to the current working directory. Args: dryrun (bool): Do not make any change to the host system if True kwargs (dict): Extra keyword arguments Returns: None """ # Check to make sure an answers.conf file doesn't exist already answers_file = os.path.join(os.getcwd(), ANSWERS_FILE) if os.path.exists(answers_file): raise NuleculeException("Can't generate answers.conf over existing file") # Call unpack to get the app code self.nulecule = self.unpack(update=False, dryrun=dryrun, config=self.config) self.nulecule.load_config(skip_asking=True) # Get answers and write them out to answers.conf in cwd answers = self._get_runtime_answers(self.nulecule.config, None) self._write_answers(answers_file, answers, self.answers_format) def fetch(self, nodeps=False, update=False, dryrun=False, **kwargs): """ Installs (unpacks) a Nulecule application from a Nulecule image to a target path. Args: answers (dict or str): Answers data or local path to answers file nodeps (bool): Install the nulecule application without installing external dependencies update (bool): Pull requisite Nulecule image and install or update already installed Nulecule application dryrun (bool): Do not make any change to the host system if True kwargs (dict): Extra keyword arguments Returns: None """ # Call unpack. If the app doesn't exist it will be pulled. If # it does exist it will be just be loaded and returned self.nulecule = self.unpack(update, dryrun, config=self.config) self.nulecule.load_config(skip_asking=True) runtime_answers = self._get_runtime_answers(self.nulecule.config, None) # write sample answers file self._write_answers(os.path.join(self.app_path, ANSWERS_FILE_SAMPLE), runtime_answers, self.answers_format) cockpit_logger.info("Install Successful.") def run(self, answers_output, ask, **kwargs): """ Runs a Nulecule application from a local path or a Nulecule image name. Args: answers (dict or str): Answers data or local path to answers file answers_output (str): Path to file to export runtime answers data to ask (bool): Ask for values for params with default values from user, if True kwargs (dict): Extra keyword arguments Returns: None """ dryrun = kwargs.get("dryrun") or False # Call unpack. If the app doesn't exist it will be pulled. If # it does exist it will be just be loaded and returned self.nulecule = self.unpack(dryrun=dryrun, config=self.config) # Process answers file self._process_answers() self.nulecule.load_config(ask=ask) provider = self.nulecule.config.get("provider") self.nulecule.render(provider, dryrun) self.nulecule.run(provider, dryrun) runtime_answers = self._get_runtime_answers(self.nulecule.config, provider) self._write_answers(os.path.join(self.app_path, ANSWERS_RUNTIME_FILE), runtime_answers, self.answers_format) if answers_output: self._write_answers(answers_output, runtime_answers, self.answers_format) def stop(self, **kwargs): """ Stops a running Nulecule application. Args: kwargs (dict): Extra keyword arguments """ # For stop we use the generated answer file from the run self.answers_file = os.path.join(self.app_path, ANSWERS_RUNTIME_FILE) self._process_answers() dryrun = kwargs.get("dryrun") or False self.nulecule = Nulecule.load_from_path(self.app_path, config=self.config, dryrun=dryrun) self.nulecule.load_config() self.nulecule.render(self.nulecule.config.get("provider"), dryrun=dryrun) self.nulecule.stop(self.nulecule.config.get("provider"), dryrun) def clean(self, force=False): # For future use self.uninstall() distutils.dir_util.remove_tree(self.unpack_path) self.initialize() def _process_answers(self): """ Processes answer files to load data from them and then merges any cli provided answers into the config. NOTE: This function should be called once on startup and then once more after the application has been extracted, but only if answers file wasn't found on the first invocation. The idea is to allow for people to embed an answers file in the application if they want, which won't be available until after extraction. Returns: None """ answers = None app_path_answers = os.path.join(self.app_path, ANSWERS_FILE) # If the user didn't provide an answers file then check the app # dir to see if one exists. if not self.answers_file: if os.path.isfile(app_path_answers): self.answers_file = app_path_answers # At this point if we have an answers file, load it if self.answers_file: # If this is a url then download answers file to app directory if urlparse.urlparse(self.answers_file).scheme != "": logger.debug("Retrieving answers file from: {}".format(self.answers_file)) with open(app_path_answers, "w+") as f: stream = urllib.urlopen(self.answers_file) f.write(stream.read()) self.answers_file = app_path_answers # Check to make sure the file exists if not os.path.isfile(self.answers_file): raise NuleculeException("Provided answers file doesn't exist: {}".format(self.answers_file)) # Load answers answers = Utils.loadAnswers(self.answers_file, self.answers_format) self.config.update_source(source="answers", data=answers) def _write_answers(self, path, answers, answers_format): """ Write answers data to file. Args: path (str): path to answers file to write to answers (dict): Answers data answers_format (str): Format to use to dump answers data to file, e.g., json Returns: None """ logger.debug("Writing answers to file.") logger.debug("FILE: %s", path) logger.debug("ANSWERS: %s", answers) logger.debug("ANSWERS FORMAT: %s", answers_format) anymarkup.serialize_file(answers, path, format=answers_format) # Make sure that the permission of the file is set to the current user Utils.setFileOwnerGroup(path) # TODO - once we rework config data we shouldn't need this # function anymore, we should be able to take the data # straight from the config object since the defaults and args # provided from the cli would have already been merged. def _get_runtime_answers(self, config, cli_provider): """ Get runtime answers data from config (Nulecule config) by adding default data if missing. Args: config (dict): Nulecule config data cli_provider (str): Provider used for running Nulecule application Returns: dict """ return self.nulecule.config.runtime_answers()
def get_answers(nulecule_path): nulecule = Nulecule.load_from_path(nulecule_path) nulecule.config = Config() nulecule.load_config(skip_asking=True) return nulecule.config.runtime_answers()
def __init__(self, app_spec, destination=None, cli_answers=None, answers_file=None, answers_format=None): """ init function for NuleculeManager. Sets a few instance variables. Args: app_spec: either a path to an unpacked nulecule app or a container image name where a nulecule can be found destination: where to unpack a nulecule to if it isn't local cli_answers: some answer file values provided from cli args answers_file: the location of the answers file answers_format (str): File format for writing sample answers file """ self.answers_format = answers_format or ANSWERS_FILE_SAMPLE_FORMAT self.answers_file = None # The path to an answer file self.app_path = None # The path where the app resides or will reside self.image = None # The container image to pull the app from # Adjust app_spec, destination, and answer file paths if absolute. if os.path.isabs(app_spec): app_spec = Utils.get_real_abspath(app_spec) if destination and os.path.isabs(destination): destination = Utils.get_real_abspath(destination) if answers_file and os.path.isabs(answers_file): answers_file = Utils.get_real_abspath(answers_file) # If the user doesn't want the files copied to a permanent # location then he provides 'none'. If that is the case we'll # use a temporary directory if destination and destination.lower() == 'none': logger.debug("'none' destination requested. Using tmp dir") destination = tempfile.mkdtemp(prefix='atomicapp') # Determine if the user passed us an image or a path to an app if not os.path.exists(app_spec): self.image = app_spec else: self.app_path = app_spec # Doesn't really make much sense to provide an app path and destination, # but if they want to we'll simply just copy the files for them if self.app_path and destination: Utils.copy_dir(self.app_path, destination, update=True) self.app_path = destination # If the user provided an image, make sure we have a destination if self.image: if destination: self.app_path = destination else: self.app_path = Utils.getNewAppCacheDir(self.image) logger.debug("NuleculeManager init app_path: %s", self.app_path) logger.debug("NuleculeManager init image: %s", self.image) # Create the app_path if it doesn't exist yet if not os.path.isdir(self.app_path): os.makedirs(self.app_path) # Set where the main nulecule file should be self.main_file = os.path.join(self.app_path, MAIN_FILE) # Process answers. self.answers_file = answers_file self.config = Config(cli=cli_answers)
class NuleculeManager(object): """ Interface to fetch, run, stop a Nulecule application. """ def __init__(self, app_spec, destination=None, cli_answers=None, answers_file=None, answers_format=None): """ init function for NuleculeManager. Sets a few instance variables. Args: app_spec: either a path to an unpacked nulecule app or a container image name where a nulecule can be found destination: where to unpack a nulecule to if it isn't local cli_answers: some answer file values provided from cli args answers_file: the location of the answers file answers_format (str): File format for writing sample answers file """ self.answers_format = answers_format or ANSWERS_FILE_SAMPLE_FORMAT self.answers_file = None # The path to an answer file self.app_path = None # The path where the app resides or will reside self.image = None # The container image to pull the app from # Adjust app_spec, destination, and answer file paths if absolute. if os.path.isabs(app_spec): app_spec = Utils.get_real_abspath(app_spec) if destination and os.path.isabs(destination): destination = Utils.get_real_abspath(destination) if answers_file and os.path.isabs(answers_file): answers_file = Utils.get_real_abspath(answers_file) # If the user doesn't want the files copied to a permanent # location then he provides 'none'. If that is the case we'll # use a temporary directory if destination and destination.lower() == 'none': logger.debug("'none' destination requested. Using tmp dir") destination = tempfile.mkdtemp(prefix='atomicapp') # Determine if the user passed us an image or a path to an app if not os.path.exists(app_spec): self.image = app_spec else: self.app_path = app_spec # Doesn't really make much sense to provide an app path and destination, # but if they want to we'll simply just copy the files for them if self.app_path and destination: Utils.copy_dir(self.app_path, destination, update=True) self.app_path = destination # If the user provided an image, make sure we have a destination if self.image: if destination: self.app_path = destination else: self.app_path = Utils.getNewAppCacheDir(self.image) logger.debug("NuleculeManager init app_path: %s", self.app_path) logger.debug("NuleculeManager init image: %s", self.image) # Create the app_path if it doesn't exist yet if not os.path.isdir(self.app_path): os.makedirs(self.app_path) # Set where the main nulecule file should be self.main_file = os.path.join(self.app_path, MAIN_FILE) # Process answers. self.answers_file = answers_file self.config = Config(cli=cli_answers) @staticmethod def init(app_name, destination=None, app_version='1.0', app_desc='App description'): """Initialize a new Nulecule app Args: app_name (str): Application name destination (str): Destination path app_version (str): Application version app_desc (str): Application description Returns: destination (str) """ # context to render template files for Atomic App context = dict(app_name=app_name, app_version=app_version, app_desc=app_desc, atomicapp_version=__ATOMICAPPVERSION__, nulecule_spec_version=__NULECULESPECVERSION__) if destination is None: destination = os.path.join('.', app_name) # Check if destination directory exists and is not empty if os.path.exists(destination) and \ os.path.isdir(destination) and os.listdir(destination): value = raw_input('Destination directory is not empty! ' 'Do you still want to proceed? [Y]/n: ') value = value or 'y' if value.lower() != 'y': return # Exit out as the user has chosen not to proceed # Temporary working dir to render the templates tmpdir = tempfile.mkdtemp(prefix='nulecule-new-app-') template_dir = os.path.join(os.path.dirname(__file__), 'external/templates/nulecule') try: # Copy template dir to temporary working directory and render templates distutils.dir_util.copy_tree(template_dir, tmpdir) for item in os.walk(tmpdir): parent_dir, dirs, files = item for filename in files: if not filename.endswith('.tpl'): continue templ_path = os.path.join(parent_dir, filename) if parent_dir.endswith('artifacts/docker') or \ parent_dir.endswith('artifacts/kubernetes'): file_path = os.path.join( parent_dir, '{}_{}'.format(app_name, filename[:-4])) else: file_path = os.path.join(parent_dir, filename[:-4]) with open(templ_path) as f: s = f.read() t = Template(s) with open(file_path, 'w') as f: f.write(t.safe_substitute(**context)) os.remove(templ_path) # Copy rendered templates to destination directory distutils.dir_util.copy_tree(tmpdir, destination, True) finally: # Remove temporary working directory distutils.dir_util.remove_tree(tmpdir) return destination def unpack(self, update=False, dryrun=False, nodeps=False, config=None): """ Unpacks a Nulecule application from a Nulecule image to a path or load a Nulecule that already exists locally. Args: update (bool): Update existing Nulecule application in app_path, if True dryrun (bool): Do not make any change to the host system nodeps (bool): Do not unpack external dependencies config (dict): Config data, if any, to use for unpacking Returns: A Nulecule instance. """ logger.debug('Request to unpack to %s to %s' % (self.image, self.app_path)) # If the user provided an image then unpack it and return the # resulting Nulecule. Else, load from existing path if self.image: return Nulecule.unpack(self.image, self.app_path, config=config, nodeps=nodeps, dryrun=dryrun, update=update) else: return Nulecule.load_from_path(self.app_path, dryrun=dryrun, config=config) def genanswers(self, dryrun=False, **kwargs): """ Renders artifacts and then generates an answer file. Finally copies answer file to the current working directory. Args: dryrun (bool): Do not make any change to the host system if True kwargs (dict): Extra keyword arguments Returns: None """ # Check to make sure an answers.conf file doesn't exist already answers_file = os.path.join(os.getcwd(), ANSWERS_FILE) if os.path.exists(answers_file): raise NuleculeException( "Can't generate answers.conf over existing file") # Call unpack to get the app code self.nulecule = self.unpack(update=False, dryrun=dryrun, config=self.config) self.nulecule.load_config(skip_asking=True) # Get answers and write them out to answers.conf in cwd answers = self._get_runtime_answers(self.nulecule.config, None) self._write_answers(answers_file, answers, self.answers_format) def fetch(self, nodeps=False, update=False, dryrun=False, **kwargs): """ Installs (unpacks) a Nulecule application from a Nulecule image to a target path. Args: answers (dict or str): Answers data or local path to answers file nodeps (bool): Install the nulecule application without installing external dependencies update (bool): Pull requisite Nulecule image and install or update already installed Nulecule application dryrun (bool): Do not make any change to the host system if True kwargs (dict): Extra keyword arguments Returns: None """ # Call unpack. If the app doesn't exist it will be pulled. If # it does exist it will be just be loaded and returned self.nulecule = self.unpack(update, dryrun, config=self.config) self.nulecule.load_config(skip_asking=True) runtime_answers = self._get_runtime_answers(self.nulecule.config, None) # write sample answers file self._write_answers(os.path.join(self.app_path, ANSWERS_FILE_SAMPLE), runtime_answers, self.answers_format) cockpit_logger.info("Install Successful.") def run(self, answers_output, ask, **kwargs): """ Runs a Nulecule application from a local path or a Nulecule image name. Args: answers (dict or str): Answers data or local path to answers file answers_output (str): Path to file to export runtime answers data to ask (bool): Ask for values for params with default values from user, if True kwargs (dict): Extra keyword arguments Returns: None """ dryrun = kwargs.get('dryrun') or False # Call unpack. If the app doesn't exist it will be pulled. If # it does exist it will be just be loaded and returned self.nulecule = self.unpack(dryrun=dryrun, config=self.config) # Process answers file self._process_answers() self.nulecule.load_config(ask=ask) provider = self.nulecule.config.get('provider') self.nulecule.render(provider, dryrun) self.nulecule.run(provider, dryrun) runtime_answers = self._get_runtime_answers(self.nulecule.config, provider) self._write_answers(os.path.join(self.app_path, ANSWERS_RUNTIME_FILE), runtime_answers, self.answers_format) if answers_output: self._write_answers(answers_output, runtime_answers, self.answers_format) def stop(self, **kwargs): """ Stops a running Nulecule application. Args: kwargs (dict): Extra keyword arguments """ # For stop we use the generated answer file from the run self.answers_file = os.path.join(self.app_path, ANSWERS_RUNTIME_FILE) self._process_answers() dryrun = kwargs.get('dryrun') or False self.nulecule = Nulecule.load_from_path(self.app_path, config=self.config, dryrun=dryrun) self.nulecule.load_config() self.nulecule.render(self.nulecule.config.get('provider'), dryrun=dryrun) self.nulecule.stop(self.nulecule.config.get('provider'), dryrun) def clean(self, force=False): # For future use self.uninstall() distutils.dir_util.remove_tree(self.unpack_path) self.initialize() def _process_answers(self): """ Processes answer files to load data from them and then merges any cli provided answers into the config. NOTE: This function should be called once on startup and then once more after the application has been extracted, but only if answers file wasn't found on the first invocation. The idea is to allow for people to embed an answers file in the application if they want, which won't be available until after extraction. Returns: None """ answers = None app_path_answers = os.path.join(self.app_path, ANSWERS_FILE) # If the user didn't provide an answers file then check the app # dir to see if one exists. if not self.answers_file: if os.path.isfile(app_path_answers): self.answers_file = app_path_answers # At this point if we have an answers file, load it if self.answers_file: # If this is a url then download answers file to app directory if urlparse.urlparse(self.answers_file).scheme != "": logger.debug("Retrieving answers file from: {}".format( self.answers_file)) with open(app_path_answers, 'w+') as f: stream = urllib.urlopen(self.answers_file) f.write(stream.read()) self.answers_file = app_path_answers # Check to make sure the file exists if not os.path.isfile(self.answers_file): raise NuleculeException( "Provided answers file doesn't exist: {}".format( self.answers_file)) # Load answers answers = Utils.loadAnswers(self.answers_file, self.answers_format) self.config.update_source(source='answers', data=answers) def _write_answers(self, path, answers, answers_format): """ Write answers data to file. Args: path (str): path to answers file to write to answers (dict): Answers data answers_format (str): Format to use to dump answers data to file, e.g., json Returns: None """ logger.debug("Writing answers to file.") logger.debug("FILE: %s", path) logger.debug("ANSWERS: %s", answers) logger.debug("ANSWERS FORMAT: %s", answers_format) anymarkup.serialize_file(answers, path, format=answers_format) # Make sure that the permission of the file is set to the current user Utils.setFileOwnerGroup(path) # TODO - once we rework config data we shouldn't need this # function anymore, we should be able to take the data # straight from the config object since the defaults and args # provided from the cli would have already been merged. def _get_runtime_answers(self, config, cli_provider): """ Get runtime answers data from config (Nulecule config) by adding default data if missing. Args: config (dict): Nulecule config data cli_provider (str): Provider used for running Nulecule application Returns: dict """ return self.nulecule.config.runtime_answers()