Repository: ansible/ansible

Issue:
Regression: ansible requires access to api server for local dependencies
##### Issue Type:

Bug Report - Regression
##### Ansible Version:

2.0.0.2 - from PPA
##### Ansible Configuration:

Other than upgrading to 2.0.0.2, no config change
##### Environment:

Ubuntu 15.10
##### Summary:

Ansible 2.0.0.2 via the ansible-galaxy command, will not pull from a local github enerprise dependencies, without an internet connection to https://galaxy.ansible.com/api/. Ansible version 1.9.4 had no issues pulling down local dependencies from github enterprise.
##### Steps To Reproduce:

Be on a network without internet access, but has hosted git repos on it (github-enterprise, etc). The playbook is not publicly hosted.

Run dependency script: command used: ansible galaxy install -r requirements.yml
##### Expected Results:

Command runs successfully, standard output
##### Actual Results:

ERROR! The API server (https://galaxy.ansible.com/api/) is not responding, please try again later.



Discussion:
Note as of Ansible 2.0.1.0 this is still broken. Since my workplace's network doesn't have internet, this is preventing any of our github-enterprise hosted roles from updating, restricting us to ansible versions pre 2.x.x.x.

After doing some searching through the issues, it looks like there is a pull request that will add an offline option for galaxy: #14486 

Without this merge, I and anyone using ansible and ansible-galaxy with an internet limited network are limitied to using pre 2.x versions of ansible.

Thanks for sharing. I guess [patching](https://patch-diff.githubusercontent.com/raw/ansible/ansible/pull/14486.patch) that PR is the way to go while we wait.

Proposed fix in #15593


Resolving patch:
diff --git a/lib/ansible/cli/galaxy.py b/lib/ansible/cli/galaxy.py
--- a/lib/ansible/cli/galaxy.py
+++ b/lib/ansible/cli/galaxy.py
@@ -51,7 +51,7 @@ class GalaxyCLI(CLI):
 
     SKIP_INFO_KEYS = ("name", "description", "readme_html", "related", "summary_fields", "average_aw_composite", "average_aw_score", "url" )
     VALID_ACTIONS = ("delete", "import", "info", "init", "install", "list", "login", "remove", "search", "setup")
-    
+
     def __init__(self, args):
         self.api = None
         self.galaxy = None
@@ -146,14 +146,10 @@ def parse(self):
         return True
 
     def run(self):
-        
-        super(GalaxyCLI, self).run()
 
-        # if not offline, get connect to galaxy api
-        if self.action in ("import","info","install","search","login","setup","delete") or \
-            (self.action == 'init' and not self.options.offline):
-            self.api = GalaxyAPI(self.galaxy)
+        super(GalaxyCLI, self).run()
 
+        self.api = GalaxyAPI(self.galaxy)
         self.execute()
 
     def exit_without_ignore(self, rc=1):
@@ -242,7 +238,7 @@ def execute_init(self):
                 # platforms included (but commented out), the galaxy_tags
                 # list, and the dependencies section
                 platforms = []
-                if not offline and self.api:
+                if not offline:
                     platforms = self.api.get_list("platforms") or []
 
                 # group the list of platforms from the api based
@@ -315,7 +311,7 @@ def execute_info(self):
                 role_info.update(install_info)
 
             remote_data = False
-            if self.api:
+            if not self.options.offline:
                 remote_data = self.api.lookup_role_by_name(role, False)
 
             if remote_data:
diff --git a/lib/ansible/galaxy/api.py b/lib/ansible/galaxy/api.py
--- a/lib/ansible/galaxy/api.py
+++ b/lib/ansible/galaxy/api.py
@@ -28,7 +28,6 @@
 import urllib
 
 from urllib2 import quote as urlquote, HTTPError
-from urlparse import urlparse
 
 import ansible.constants as C
 from ansible.errors import AnsibleError
@@ -41,6 +40,21 @@
     from ansible.utils.display import Display
     display = Display()
 
+def g_connect(method):
+    ''' wrapper to lazily initialize connection info to galaxy '''
+    def wrapped(self, *args, **kwargs):
+        if not self.initialized:
+            display.vvvv("Initial connection to galaxy_server: %s" % self._api_server)
+            server_version = self._get_server_api_version()
+            if not server_version in self.SUPPORTED_VERSIONS:
+                raise AnsibleError("Unsupported Galaxy server API version: %s" % server_version)
+
+            self.baseurl = '%s/api/%s' % (self._api_server, server_version)
+            self.version = server_version # for future use
+            display.vvvv("Base API: %s" % self.baseurl)
+            self.initialized = True
+        return method(self, *args, **kwargs)
+    return wrapped
 
 class GalaxyAPI(object):
     ''' This class is meant to be used as a API client for an Ansible Galaxy server '''
@@ -52,6 +66,9 @@ def __init__(self, galaxy):
         self.token = GalaxyToken()
         self._api_server = C.GALAXY_SERVER
         self._validate_certs = not C.GALAXY_IGNORE_CERTS
+        self.baseurl = None
+        self.version = None
+        self.initialized = False
 
         # set validate_certs
         if galaxy.options.ignore_certs:
@@ -61,15 +78,7 @@ def __init__(self, galaxy):
         # set the API server
         if galaxy.options.api_server != C.GALAXY_SERVER:
             self._api_server = galaxy.options.api_server
-        display.vvv("Connecting to galaxy_server: %s" % self._api_server)
 
-        server_version = self.get_server_api_version()
-        if not server_version in self.SUPPORTED_VERSIONS:
-            raise AnsibleError("Unsupported Galaxy server API version: %s" % server_version)
-
-        self.baseurl = '%s/api/%s' % (self._api_server, server_version)
-        self.version = server_version # for future use
-        display.vvv("Base API: %s" % self.baseurl)           
 
     def __auth_header(self):
         token = self.token.get()
@@ -77,6 +86,7 @@ def __auth_header(self):
             raise AnsibleError("No access token. You must first use login to authenticate and obtain an access token.")
         return {'Authorization': 'Token ' + token}
 
+    @g_connect
     def __call_galaxy(self, url, args=None, headers=None, method=None):
         if args and not headers:
             headers = self.__auth_header()
@@ -91,13 +101,13 @@ def __call_galaxy(self, url, args=None, headers=None, method=None):
 
     @property
     def api_server(self):
-        return self._api_server    
-    
+        return self._api_server
+
     @property
     def validate_certs(self):
         return self._validate_certs
 
-    def get_server_api_version(self):
+    def _get_server_api_version(self):
         """
         Fetches the Galaxy API current version to ensure
         the API server is up and reachable.
@@ -107,8 +117,9 @@ def get_server_api_version(self):
             data = json.load(open_url(url, validate_certs=self._validate_certs))
             return data['current_version']
         except Exception as e:
-            raise AnsibleError("The API server (%s) is not responding, please try again later." % url)
-        
+            raise AnsibleError("The API server (%s) is not responding, please try again later" % url)
+
+    @g_connect
     def authenticate(self, github_token):
         """
         Retrieve an authentication token
@@ -134,6 +145,7 @@ def create_import_task(self, github_user, github_repo, reference=None):
             return data['results']
         return data
 
+    @g_connect
     def get_import_task(self, task_id=None, github_user=None, github_repo=None):
         """
         Check the status of an import task.
@@ -145,10 +157,11 @@ def get_import_task(self, task_id=None, github_user=None, github_repo=None):
             url = "%s?github_user=%s&github_repo=%s" % (url,github_user,github_repo)
         else:
             raise AnsibleError("Expected task_id or github_user and github_repo")
-        
+
         data = self.__call_galaxy(url)
         return data['results']
-       
+
+    @g_connect
     def lookup_role_by_name(self, role_name, notify=True):
         """
         Find a role by name.
@@ -170,6 +183,7 @@ def lookup_role_by_name(self, role_name, notify=True):
             return data["results"][0]
         return None
 
+    @g_connect
     def fetch_role_related(self, related, role_id):
         """
         Fetch the list of related items for the given role.
@@ -190,6 +204,7 @@ def fetch_role_related(self, related, role_id):
         except:
             return None
 
+    @g_connect
     def get_list(self, what):
         """
         Fetch the list of items specified.
@@ -213,6 +228,7 @@ def get_list(self, what):
         except Exception as error:
             raise AnsibleError("Failed to download the %s list: %s" % (what, str(error)))
 
+    @g_connect
     def search_roles(self, search, **kwargs):
 
         search_url = self.baseurl + '/search/roles/?'
@@ -228,7 +244,7 @@ def search_roles(self, search, **kwargs):
         if tags and isinstance(tags, basestring):
             tags = tags.split(',')
             search_url += '&tags_autocomplete=' + '+'.join(tags)
-        
+
         if platforms and isinstance(platforms, basestring):
             platforms = platforms.split(',')
             search_url += '&platforms_autocomplete=' + '+'.join(platforms)
@@ -238,10 +254,11 @@ def search_roles(self, search, **kwargs):
 
         if author:
             search_url += '&username_autocomplete=%s' % author
-   
+
         data = self.__call_galaxy(search_url)
         return data
 
+    @g_connect
     def add_secret(self, source, github_user, github_repo, secret):
         url = "%s/notification_secrets/" % self.baseurl
         args = urllib.urlencode({
@@ -253,16 +270,19 @@ def add_secret(self, source, github_user, github_repo, secret):
         data = self.__call_galaxy(url, args=args)
         return data
 
+    @g_connect
     def list_secrets(self):
         url = "%s/notification_secrets" % self.baseurl
         data = self.__call_galaxy(url, headers=self.__auth_header())
         return data
 
+    @g_connect
     def remove_secret(self, secret_id):
         url = "%s/notification_secrets/%s/" % (self.baseurl, secret_id)
         data = self.__call_galaxy(url, headers=self.__auth_header(), method='DELETE')
         return data
 
+    @g_connect
     def delete_role(self, github_user, github_repo):
         url = "%s/removerole/?github_user=%s&github_repo=%s" % (self.baseurl,github_user,github_repo)
         data = self.__call_galaxy(url, headers=self.__auth_header(), method='DELETE')


Test patch: