|
|
|
@ -21,15 +21,16 @@
|
|
|
|
|
|
|
|
|
|
import datetime
|
|
|
|
|
import os
|
|
|
|
|
import subprocess
|
|
|
|
|
import tarfile
|
|
|
|
|
import tempfile
|
|
|
|
|
import yaml
|
|
|
|
|
from distutils.version import LooseVersion
|
|
|
|
|
from shutil import rmtree
|
|
|
|
|
|
|
|
|
|
from ansible import constants as C
|
|
|
|
|
from ansible.errors import AnsibleError
|
|
|
|
|
from ansible.module_utils.urls import open_url
|
|
|
|
|
from ansible.playbook.role.requirement import RoleRequirement
|
|
|
|
|
from ansible.galaxy.api import GalaxyAPI
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
from __main__ import display
|
|
|
|
@ -51,6 +52,7 @@ class GalaxyRole(object):
|
|
|
|
|
self._install_info = None
|
|
|
|
|
|
|
|
|
|
self.options = galaxy.options
|
|
|
|
|
self.galaxy = galaxy
|
|
|
|
|
|
|
|
|
|
self.name = name
|
|
|
|
|
self.version = version
|
|
|
|
@ -135,9 +137,9 @@ class GalaxyRole(object):
|
|
|
|
|
|
|
|
|
|
def remove(self):
|
|
|
|
|
"""
|
|
|
|
|
Removes the specified role from the roles path. There is a
|
|
|
|
|
sanity check to make sure there's a meta/main.yml file at this
|
|
|
|
|
path so the user doesn't blow away random directories
|
|
|
|
|
Removes the specified role from the roles path.
|
|
|
|
|
There is a sanity check to make sure there's a meta/main.yml file at this
|
|
|
|
|
path so the user doesn't blow away random directories.
|
|
|
|
|
"""
|
|
|
|
|
if self.metadata:
|
|
|
|
|
try:
|
|
|
|
@ -159,6 +161,7 @@ class GalaxyRole(object):
|
|
|
|
|
archive_url = 'https://github.com/%s/%s/archive/%s.tar.gz' % (role_data["github_user"], role_data["github_repo"], self.version)
|
|
|
|
|
else:
|
|
|
|
|
archive_url = self.src
|
|
|
|
|
|
|
|
|
|
display.display("- downloading role from %s" % archive_url)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
@ -170,87 +173,125 @@ class GalaxyRole(object):
|
|
|
|
|
data = url_file.read()
|
|
|
|
|
temp_file.close()
|
|
|
|
|
return temp_file.name
|
|
|
|
|
except:
|
|
|
|
|
# TODO: better urllib2 error handling for error
|
|
|
|
|
# messages that are more exact
|
|
|
|
|
display.error("failed to download the file.")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
display.error("failed to download the file: %s" % str(e))
|
|
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def install(self, role_filename):
|
|
|
|
|
def install(self):
|
|
|
|
|
# the file is a tar, so open it that way and extract it
|
|
|
|
|
# to the specified (or default) roles directory
|
|
|
|
|
|
|
|
|
|
if not tarfile.is_tarfile(role_filename):
|
|
|
|
|
display.error("the file downloaded was not a tar.gz")
|
|
|
|
|
return False
|
|
|
|
|
else:
|
|
|
|
|
if role_filename.endswith('.gz'):
|
|
|
|
|
role_tar_file = tarfile.open(role_filename, "r:gz")
|
|
|
|
|
if self.scm:
|
|
|
|
|
# create tar file from scm url
|
|
|
|
|
tmp_file = RoleRequirement.scm_archive_role(**self.spec)
|
|
|
|
|
elif self.src:
|
|
|
|
|
if os.path.isfile(self.src):
|
|
|
|
|
# installing a local tar.gz
|
|
|
|
|
tmp_file = self.src
|
|
|
|
|
elif '://' in self.src:
|
|
|
|
|
role_data = self.src
|
|
|
|
|
tmp_file = self.fetch(role_data)
|
|
|
|
|
else:
|
|
|
|
|
role_tar_file = tarfile.open(role_filename, "r")
|
|
|
|
|
# verify the role's meta file
|
|
|
|
|
meta_file = None
|
|
|
|
|
members = role_tar_file.getmembers()
|
|
|
|
|
# next find the metadata file
|
|
|
|
|
for member in members:
|
|
|
|
|
if self.META_MAIN in member.name:
|
|
|
|
|
meta_file = member
|
|
|
|
|
break
|
|
|
|
|
if not meta_file:
|
|
|
|
|
display.error("this role does not appear to have a meta/main.yml file.")
|
|
|
|
|
return False
|
|
|
|
|
api = GalaxyAPI(self.galaxy, self.options.api_server)
|
|
|
|
|
role_data = api.lookup_role_by_name(self.src)
|
|
|
|
|
if not role_data:
|
|
|
|
|
raise AnsibleError("- sorry, %s was not found on %s." % (self.src, self.options.api_server))
|
|
|
|
|
|
|
|
|
|
role_versions = api.fetch_role_related('versions', role_data['id'])
|
|
|
|
|
if not self.version:
|
|
|
|
|
# convert the version names to LooseVersion objects
|
|
|
|
|
# and sort them to get the latest version. If there
|
|
|
|
|
# are no versions in the list, we'll grab the head
|
|
|
|
|
# of the master branch
|
|
|
|
|
if len(role_versions) > 0:
|
|
|
|
|
loose_versions = [LooseVersion(a.get('name',None)) for a in role_versions]
|
|
|
|
|
loose_versions.sort()
|
|
|
|
|
self.version = str(loose_versions[-1])
|
|
|
|
|
else:
|
|
|
|
|
self.version = 'master'
|
|
|
|
|
elif self.version != 'master':
|
|
|
|
|
if role_versions and self.version not in [a.get('name', None) for a in role_versions]:
|
|
|
|
|
raise AnsibleError("- the specified version (%s) of %s was not found in the list of available versions (%s)." % (self.version, self.name, role_versions))
|
|
|
|
|
|
|
|
|
|
tmp_file = self.fetch(role_data)
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
raise AnsibleError("No valid role data found")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if tmp_file:
|
|
|
|
|
|
|
|
|
|
display.display("installing from %s" % tmp_file)
|
|
|
|
|
|
|
|
|
|
if not tarfile.is_tarfile(tmp_file):
|
|
|
|
|
raise AnsibleError("the file downloaded was not a tar.gz")
|
|
|
|
|
else:
|
|
|
|
|
if tmp_file.endswith('.gz'):
|
|
|
|
|
role_tar_file = tarfile.open(tmp_file, "r:gz")
|
|
|
|
|
else:
|
|
|
|
|
role_tar_file = tarfile.open(tmp_file, "r")
|
|
|
|
|
# verify the role's meta file
|
|
|
|
|
meta_file = None
|
|
|
|
|
members = role_tar_file.getmembers()
|
|
|
|
|
# next find the metadata file
|
|
|
|
|
for member in members:
|
|
|
|
|
if self.META_MAIN in member.name:
|
|
|
|
|
meta_file = member
|
|
|
|
|
break
|
|
|
|
|
if not meta_file:
|
|
|
|
|
raise AnsibleError("this role does not appear to have a meta/main.yml file.")
|
|
|
|
|
else:
|
|
|
|
|
try:
|
|
|
|
|
self._metadata = yaml.safe_load(role_tar_file.extractfile(meta_file))
|
|
|
|
|
except:
|
|
|
|
|
raise AnsibleError("this role does not appear to have a valid meta/main.yml file.")
|
|
|
|
|
|
|
|
|
|
# we strip off the top-level directory for all of the files contained within
|
|
|
|
|
# the tar file here, since the default is 'github_repo-target', and change it
|
|
|
|
|
# to the specified role's name
|
|
|
|
|
display.display("- extracting %s to %s" % (self.name, self.path))
|
|
|
|
|
try:
|
|
|
|
|
self._metadata = yaml.safe_load(role_tar_file.extractfile(meta_file))
|
|
|
|
|
except:
|
|
|
|
|
display.error("this role does not appear to have a valid meta/main.yml file.")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# we strip off the top-level directory for all of the files contained within
|
|
|
|
|
# the tar file here, since the default is 'github_repo-target', and change it
|
|
|
|
|
# to the specified role's name
|
|
|
|
|
display.display("- extracting %s to %s" % (self.name, self.path))
|
|
|
|
|
try:
|
|
|
|
|
if os.path.exists(self.path):
|
|
|
|
|
if not os.path.isdir(self.path):
|
|
|
|
|
display.error("the specified roles path exists and is not a directory.")
|
|
|
|
|
return False
|
|
|
|
|
elif not getattr(self.options, "force", False):
|
|
|
|
|
display.error("the specified role %s appears to already exist. Use --force to replace it." % self.name)
|
|
|
|
|
return False
|
|
|
|
|
if os.path.exists(self.path):
|
|
|
|
|
if not os.path.isdir(self.path):
|
|
|
|
|
raise AnsibleError("the specified roles path exists and is not a directory.")
|
|
|
|
|
elif not getattr(self.options, "force", False):
|
|
|
|
|
raise AnsibleError("the specified role %s appears to already exist. Use --force to replace it." % self.name)
|
|
|
|
|
else:
|
|
|
|
|
# using --force, remove the old path
|
|
|
|
|
if not self.remove():
|
|
|
|
|
raise AnsibleError("%s doesn't appear to contain a role.\n please remove this directory manually if you really want to put the role here." % self.path)
|
|
|
|
|
else:
|
|
|
|
|
# using --force, remove the old path
|
|
|
|
|
if not self.remove():
|
|
|
|
|
display.error("%s doesn't appear to contain a role." % self.path)
|
|
|
|
|
display.error(" please remove this directory manually if you really want to put the role here.")
|
|
|
|
|
return False
|
|
|
|
|
else:
|
|
|
|
|
os.makedirs(self.path)
|
|
|
|
|
os.makedirs(self.path)
|
|
|
|
|
|
|
|
|
|
# now we do the actual extraction to the path
|
|
|
|
|
for member in members:
|
|
|
|
|
# we only extract files, and remove any relative path
|
|
|
|
|
# bits that might be in the file for security purposes
|
|
|
|
|
# and drop the leading directory, as mentioned above
|
|
|
|
|
if member.isreg() or member.issym():
|
|
|
|
|
parts = member.name.split(os.sep)[1:]
|
|
|
|
|
final_parts = []
|
|
|
|
|
for part in parts:
|
|
|
|
|
if part != '..' and '~' not in part and '$' not in part:
|
|
|
|
|
final_parts.append(part)
|
|
|
|
|
member.name = os.path.join(*final_parts)
|
|
|
|
|
role_tar_file.extract(member, self.path)
|
|
|
|
|
|
|
|
|
|
# write out the install info file for later use
|
|
|
|
|
self._write_galaxy_install_info()
|
|
|
|
|
except OSError as e:
|
|
|
|
|
raise AnsibleError("Could not update files in %s: %s" % (self.path, str(e)))
|
|
|
|
|
|
|
|
|
|
# return the parsed yaml metadata
|
|
|
|
|
display.display("- %s was installed successfully" % self.name)
|
|
|
|
|
try:
|
|
|
|
|
os.unlink(tmp_file)
|
|
|
|
|
except (OSError,IOError) as e:
|
|
|
|
|
display.warning("Unable to remove tmp file (%s): %s" % (tmp_file, str(e)))
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
# now we do the actual extraction to the path
|
|
|
|
|
for member in members:
|
|
|
|
|
# we only extract files, and remove any relative path
|
|
|
|
|
# bits that might be in the file for security purposes
|
|
|
|
|
# and drop the leading directory, as mentioned above
|
|
|
|
|
if member.isreg() or member.issym():
|
|
|
|
|
parts = member.name.split(os.sep)[1:]
|
|
|
|
|
final_parts = []
|
|
|
|
|
for part in parts:
|
|
|
|
|
if part != '..' and '~' not in part and '$' not in part:
|
|
|
|
|
final_parts.append(part)
|
|
|
|
|
member.name = os.path.join(*final_parts)
|
|
|
|
|
role_tar_file.extract(member, self.path)
|
|
|
|
|
|
|
|
|
|
# write out the install info file for later use
|
|
|
|
|
self._write_galaxy_install_info()
|
|
|
|
|
except OSError as e:
|
|
|
|
|
display.error("Could not update files in %s: %s" % (self.path, str(e)))
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# return the parsed yaml metadata
|
|
|
|
|
display.display("- %s was installed successfully" % self.name)
|
|
|
|
|
return True
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def spec(self):
|
|
|
|
@ -266,65 +307,3 @@ class GalaxyRole(object):
|
|
|
|
|
return dict(scm=self.scm, src=self.src, version=self.version, name=self.name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def url_to_spec(roleurl):
|
|
|
|
|
# gets the role name out of a repo like
|
|
|
|
|
# http://git.example.com/repos/repo.git" => "repo"
|
|
|
|
|
|
|
|
|
|
if '://' not in roleurl and '@' not in roleurl:
|
|
|
|
|
return roleurl
|
|
|
|
|
trailing_path = roleurl.split('/')[-1]
|
|
|
|
|
if trailing_path.endswith('.git'):
|
|
|
|
|
trailing_path = trailing_path[:-4]
|
|
|
|
|
if trailing_path.endswith('.tar.gz'):
|
|
|
|
|
trailing_path = trailing_path[:-7]
|
|
|
|
|
if ',' in trailing_path:
|
|
|
|
|
trailing_path = trailing_path.split(',')[0]
|
|
|
|
|
return trailing_path
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def scm_archive_role(scm, role_url, role_version, role_name):
|
|
|
|
|
if scm not in ['hg', 'git']:
|
|
|
|
|
display.display("- scm %s is not currently supported" % scm)
|
|
|
|
|
return False
|
|
|
|
|
tempdir = tempfile.mkdtemp()
|
|
|
|
|
clone_cmd = [scm, 'clone', role_url, role_name]
|
|
|
|
|
with open('/dev/null', 'w') as devnull:
|
|
|
|
|
try:
|
|
|
|
|
display.display("- executing: %s" % " ".join(clone_cmd))
|
|
|
|
|
popen = subprocess.Popen(clone_cmd, cwd=tempdir, stdout=devnull, stderr=devnull)
|
|
|
|
|
except:
|
|
|
|
|
raise AnsibleError("error executing: %s" % " ".join(clone_cmd))
|
|
|
|
|
rc = popen.wait()
|
|
|
|
|
if rc != 0:
|
|
|
|
|
display.display("- command %s failed" % ' '.join(clone_cmd))
|
|
|
|
|
display.display(" in directory %s" % tempdir)
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.tar')
|
|
|
|
|
if scm == 'hg':
|
|
|
|
|
archive_cmd = ['hg', 'archive', '--prefix', "%s/" % role_name]
|
|
|
|
|
if role_version:
|
|
|
|
|
archive_cmd.extend(['-r', role_version])
|
|
|
|
|
archive_cmd.append(temp_file.name)
|
|
|
|
|
if scm == 'git':
|
|
|
|
|
archive_cmd = ['git', 'archive', '--prefix=%s/' % role_name, '--output=%s' % temp_file.name]
|
|
|
|
|
if role_version:
|
|
|
|
|
archive_cmd.append(role_version)
|
|
|
|
|
else:
|
|
|
|
|
archive_cmd.append('HEAD')
|
|
|
|
|
|
|
|
|
|
with open('/dev/null', 'w') as devnull:
|
|
|
|
|
display.display("- executing: %s" % " ".join(archive_cmd))
|
|
|
|
|
popen = subprocess.Popen(archive_cmd, cwd=os.path.join(tempdir, role_name),
|
|
|
|
|
stderr=devnull, stdout=devnull)
|
|
|
|
|
rc = popen.wait()
|
|
|
|
|
if rc != 0:
|
|
|
|
|
display.display("- command %s failed" % ' '.join(archive_cmd))
|
|
|
|
|
display.display(" in directory %s" % tempdir)
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
rmtree(tempdir, ignore_errors=True)
|
|
|
|
|
|
|
|
|
|
return temp_file.name
|
|
|
|
|
|
|
|
|
|