Completed
Pull Request — master (#2836)
by Edward
32:11
created

DownloadGitRepoAction._get_pack_name_and_url()   A

Complexity

Conditions 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
c 2
b 0
f 0
dl 0
loc 7
rs 9.4285
1
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
2
# contributor license agreements.  See the NOTICE file distributed with
3
# this work for additional information regarding copyright ownership.
4
# The ASF licenses this file to You under the Apache License, Version 2.0
5
# (the "License"); you may not use this file except in compliance with
6
# the License.  You may obtain a copy of the License at
7
#
8
#     http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
16
import os
17
import shutil
18
import hashlib
19
import stat
20
21
import six
22
from git.repo import Repo
23
from lockfile import LockFile
24
25
from st2actions.runners.pythonrunner import Action
26
from st2common.util.green import shell
27
28
MANIFEST_FILE = 'pack.yaml'
29
CONFIG_FILE = 'config.yaml'
30
GITINFO_FILE = '.gitinfo'
31
PACK_RESERVE_CHARACTER = '.'
32
33
PACK_GROUP_CFG_KEY = 'pack_group'
34
EXCHANGE_URL_KEY = 'exchange_url'
35
36
37
class DownloadGitRepoAction(Action):
38
    def __init__(self, config=None, action_service=None):
39
        super(DownloadGitRepoAction, self).__init__(config=config, action_service=action_service)
40
41
    def run(self, pack, version, abs_repo_base, verifyssl=True):
42
43
        self._pack_name, self._pack_url = self._get_pack_name_and_url(
44
            pack,
45
            self.config.get(EXCHANGE_URL_KEY, None)
46
        )
47
48
        lock_name = hashlib.md5(self._pack_name).hexdigest() + '.lock'
49
50
        with LockFile('/tmp/%s' % (lock_name)):
51
            abs_local_path = self._clone_repo(repo_name=self._pack_name, repo_url=self._pack_url,
52
                                              verifyssl=verifyssl, branch=version)
53
            try:
54
                result = self._move_pack(abs_repo_base, self._pack_name, abs_local_path)
55
            finally:
56
                self._cleanup_repo(abs_local_path)
57
58
        return self._validate_result(result=result, repo_url=self._pack_url)
59
60
    @staticmethod
61
    def _clone_repo(repo_name, repo_url, verifyssl=True, branch='master'):
62
        user_home = os.path.expanduser('~')
63
        abs_local_path = os.path.join(user_home, repo_name)
64
65
        # Disable SSL cert checking if explictly asked
66
        if not verifyssl:
67
            os.environ['GIT_SSL_NO_VERIFY'] = 'true'
68
        # Shallow clone the repo to avoid getting all the metadata. We only need HEAD of a
69
        # specific branch so save some download time.
70
        Repo.clone_from(repo_url, abs_local_path, branch=branch, depth=1)
71
        return abs_local_path
72
73
    def _move_pack(self, abs_repo_base, pack_name, abs_local_path):
74
        result = {}
75
76
        desired, message = DownloadGitRepoAction._is_desired_pack(abs_local_path, pack_name)
77
        if desired:
78
            to = abs_repo_base
79
            dest_pack_path = os.path.join(abs_repo_base, pack_name)
80
            if os.path.exists(dest_pack_path):
81
                self.logger.debug('Removing existing pack %s in %s to replace.', pack_name,
82
                                  dest_pack_path)
83
84
                # Ensure to preserve any existing configuration
85
                old_config_file = os.path.join(dest_pack_path, CONFIG_FILE)
86
                new_config_file = os.path.join(abs_local_path, CONFIG_FILE)
87
88
                if os.path.isfile(old_config_file):
89
                    shutil.move(old_config_file, new_config_file)
90
91
                shutil.rmtree(dest_pack_path)
92
93
            self.logger.debug('Moving pack from %s to %s.', abs_local_path, to)
94
            shutil.move(abs_local_path, to)
95
            # post move fix all permissions.
96
            self._apply_pack_permissions(pack_path=dest_pack_path)
97
            message = 'Success.'
98
        elif message:
99
            message = 'Failure : %s' % message
100
101
        result[pack_name] = (desired, message)
102
103
        return result
104
105
    def _apply_pack_permissions(self, pack_path):
106
        """
107
        Will recursively apply permission 770 to pack and its contents.
108
        """
109
        # 1. switch owner group to configured group
110
        pack_group = self.config.get(PACK_GROUP_CFG_KEY, None)
111
        if pack_group:
112
            shell.run_command(['sudo', 'chgrp', '-R', pack_group, pack_path])
113
114
        # 2. Setup the right permissions and group ownership
115
        # These mask is same as mode = 775
116
        mode = stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH | stat.S_IXOTH
117
        os.chmod(pack_path, mode)
118
119
        # Yuck! Since os.chmod does not support chmod -R walk manually.
120
        for root, dirs, files in os.walk(pack_path):
121
            for d in dirs:
122
                os.chmod(os.path.join(root, d), mode)
123
            for f in files:
124
                os.chmod(os.path.join(root, f), mode)
125
126
    @staticmethod
127
    def _is_desired_pack(abs_pack_path, pack_name):
128
        # path has to exist.
129
        if not os.path.exists(abs_pack_path):
130
            return (False, 'Pack "%s" not found or it\'s missing a "pack.yaml" file.' %
131
                    (pack_name))
132
        # should not include reserve characters
133
        if PACK_RESERVE_CHARACTER in pack_name:
134
            return (False, 'Pack name "%s" contains reserve character "%s"' %
135
                    (pack_name, PACK_RESERVE_CHARACTER))
136
        # must contain a manifest file. Empty file is ok for now.
137
        if not os.path.isfile(os.path.join(abs_pack_path, MANIFEST_FILE)):
138
            return (False, 'Pack is missing a manifest file (%s).' % (MANIFEST_FILE))
139
        return (True, '')
140
141
    @staticmethod
142
    def _cleanup_repo(abs_local_path):
143
        # basic lock checking etc?
144
        if os.path.isdir(abs_local_path):
145
            shutil.rmtree(abs_local_path)
146
147
    @staticmethod
148
    def _validate_result(result, repo_url):
149
        atleast_one_success = False
150
        sanitized_result = {}
151
        for k, v in six.iteritems(result):
152
            atleast_one_success |= v[0]
153
            sanitized_result[k] = v[1]
154
155
        if not atleast_one_success:
156
            message_list = []
157
            message_list.append('The pack has not been downloaded from "%s".\n' % (repo_url))
158
            message_list.append('Errors:')
159
160
            for pack, value in result.items():
161
                success, error = value
162
                message_list.append(error)
163
164
            message = '\n'.join(message_list)
165
            raise Exception(message)
166
167
        return sanitized_result
168
169
    @staticmethod
170
    def _get_pack_name_and_url(name_or_url, exchange_url):
171
        if len(name_or_url.split('/')) == 1:
172
            return (name_or_url, "{}/{}.git".format(exchange_url, name_or_url))
173
        else:
174
            return (DownloadGitRepoAction._eval_repo_name(name_or_url),
175
                    DownloadGitRepoAction._eval_repo_url(name_or_url))
176
177
    @staticmethod
178
    def _eval_repo_url(repo_url):
179
        """Allow passing short GitHub style URLs"""
180
        if not repo_url:
181
            raise Exception('No valid repo_url provided or could be inferred.')
182
        has_git_extension = repo_url.endswith('.git')
183
        if len(repo_url.split('/')) == 2 and "git@" not in repo_url:
184
            url = "https://github.com/{}".format(repo_url)
185
        else:
186
            url = repo_url
187
        return url if has_git_extension else "{}.git".format(url)
188
189
    @staticmethod
190
    def _eval_repo_name(repo_url):
191
        """
192
        Evaluate the name of the repo.
193
        https://github.com/StackStorm/st2contrib.git -> st2contrib
194
        https://github.com/StackStorm/st2contrib -> st2contrib
195
        [email protected]:StackStorm/st2contrib.git -> st2contrib
196
        [email protected]:StackStorm/st2contrib -> st2contrib
197
        """
198
        last_forward_slash = repo_url.rfind('/')
199
        next_dot = repo_url.find('.', last_forward_slash)
200
        # If dot does not follow last_forward_slash return till the end
201
        if next_dot < last_forward_slash:
202
            return repo_url[last_forward_slash + 1:]
203
        return repo_url[last_forward_slash + 1:next_dot]
204