Completed
Pull Request — master (#2836)
by Edward
07:20 queued 50s
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
from git.repo import Repo
22
from lockfile import LockFile
23
24
from st2actions.runners.pythonrunner import Action
25
from st2common.util.green import shell
26
27
28
MANIFEST_FILE = 'pack.yaml'
29
CONFIG_FILE = 'config.yaml'
30
GITINFO_FILE = '.gitinfo'
31
PACK_RESERVE_CHARACTER = '.'
32
33
34
PACK_GROUP_CFG_KEY = 'pack_group'
35
EXCHANGE_URL_KEY = 'exchange_url'
36
37
38
class DownloadGitRepoAction(Action):
39
    def __init__(self, config=None, action_service=None):
40
        super(DownloadGitRepoAction, self).__init__(config=config, action_service=action_service)
41
        self._subtree = None
42
        self._repo_url = None
43
44
    def run(self, name_or_url, ref, abs_repo_base, verifyssl=True, branch='master'):
45
46
        self._pack_name, self._pack_url = self._get_pack_name_and_url(
47
            name_or_url,
48
            self.config.get(EXCHANGE_URL_KEY, None)
49
        )
50
51
        lock_name = hashlib.md5(self._pack_name).hexdigest() + '.lock'
52
53
        with LockFile('/tmp/%s' % (lock_name)):
54
            abs_local_path = self._clone_repo(repo_name=self._pack_name, repo_url=self._pack_url,
55
                                              verifyssl=verifyssl, branch=ref)
56
            try:
57
                result = self._move_pack(abs_repo_base, self._pack_name, abs_local_path)
58
            finally:
59
                self._cleanup_repo(abs_local_path)
60
61
        return self._validate_result(result=result, repo_url=self._pack_url)
62
63
    @staticmethod
64
    def _clone_repo(repo_name, repo_url, verifyssl=True, branch='master'):
65
        user_home = os.path.expanduser('~')
66
        abs_local_path = os.path.join(user_home, repo_name)
67
68
        # Disable SSL cert checking if explictly asked
69
        if not verifyssl:
70
            os.environ['GIT_SSL_NO_VERIFY'] = 'true'
71
        # Shallow clone the repo to avoid getting all the metadata. We only need HEAD of a
72
        # specific branch so save some download time.
73
        Repo.clone_from(repo_url, abs_local_path, branch=branch, depth=1)
74
        return abs_local_path
75
76
    def _apply_pack_permissions(self, pack_path):
77
        """
78
        Will recursively apply permission 770 to pack and its contents.
79
        """
80
        # 1. switch owner group to configured group
81
        pack_group = self.config.get(PACK_GROUP_CFG_KEY, None)
82
        if pack_group:
83
            shell.run_command(['sudo', 'chgrp', '-R', pack_group, pack_path])
84
85
        # 2. Setup the right permissions and group ownership
86
        # These mask is same as mode = 775
87
        mode = stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH | stat.S_IXOTH
88
        os.chmod(pack_path, mode)
89
90
        # Yuck! Since os.chmod does not support chmod -R walk manually.
91
        for root, dirs, files in os.walk(pack_path):
92
            for d in dirs:
93
                os.chmod(os.path.join(root, d), mode)
94
            for f in files:
95
                os.chmod(os.path.join(root, f), mode)
96
97
    @staticmethod
98
    def _is_desired_pack(abs_pack_path, pack_name):
99
        # should not include reserve characters
100
        if PACK_RESERVE_CHARACTER in pack_name:
101
            return (False, 'Pack name "%s" contains reserve character "%s"' %
102
                    (pack_name, PACK_RESERVE_CHARACTER))
103
        # must contain a manifest file. Empty file is ok for now.
104
        if not os.path.isfile(os.path.join(abs_pack_path, MANIFEST_FILE)):
105
            return (False, 'Pack is missing a manifest file (%s).' % (MANIFEST_FILE))
106
        return (True, '')
107
108
    @staticmethod
109
    def _cleanup_repo(abs_local_path):
110
        # basic lock checking etc?
111
        if os.path.isdir(abs_local_path):
112
            shutil.rmtree(abs_local_path)
113
114
    @staticmethod
115
    def _validate_result(result, repo_url):
116
        atleast_one_success = False
117
        sanitized_result = {}
118
        for k, v in six.iteritems(result):
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'six'
Loading history...
119
            atleast_one_success |= v[0]
120
            sanitized_result[k] = v[1]
121
122
        if not atleast_one_success:
123
            message_list = []
124
            message_list.append('The pack has not been downloaded from "%s".\n' % (repo_url))
125
            message_list.append('Errors:')
126
127
            for pack, value in result.items():
128
                success, error = value
129
                message_list.append(error)
130
131
            message = '\n'.join(message_list)
132
            raise Exception(message)
133
134
        return sanitized_result
135
136
    def _move_pack(self, abs_repo_base, pack_name, abs_local_path):
137
        result = {}
138
139
        desired, message = DownloadGitRepoAction._is_desired_pack(abs_local_path, pack_name)
140
        if desired:
141
            to = abs_repo_base
142
            dest_pack_path = os.path.join(abs_repo_base, pack_name)
143
            if os.path.exists(dest_pack_path):
144
                self.logger.debug('Removing existing pack %s in %s to replace.', pack_name,
145
                                  dest_pack_path)
146
147
                # Ensure to preserve any existing configuration
148
                old_config_file = os.path.join(dest_pack_path, CONFIG_FILE)
149
                new_config_file = os.path.join(abs_local_path, CONFIG_FILE)
150
151
                if os.path.isfile(old_config_file):
152
                    shutil.move(old_config_file, new_config_file)
153
154
                shutil.rmtree(dest_pack_path)
155
156
            self.logger.debug('Moving pack from %s to %s.', abs_local_path, to)
157
            shutil.move(abs_local_path, to)
158
            # post move fix all permissions.
159
            self._apply_pack_permissions(pack_path=dest_pack_path)
160
            message = 'Success.'
161
        elif message:
162
            message = 'Failure : %s' % message
163
164
        result[pack_name] = (desired, message)
165
166
        return result
167
168
    @staticmethod
169
    def _get_pack_name_and_url(name_or_url, exchange_url):
170
        if len(name_or_url.split('/')) == 1:
171
            return (name_or_url, "{}/{}.git".format(exchange_url, name_or_url))
172
        else:
173
            return (DownloadGitRepoAction._eval_repo_name(name_or_url),
174
                    DownloadGitRepoAction._eval_repo_url(name_or_url))
175
176
    @staticmethod
177
    def _eval_repo_url(repo_url):
178
        """Allow passing short GitHub style URLs"""
179
        if not repo_url:
180
            raise Exception('No valid repo_url provided or could be inferred.')
181
        has_git_extension = repo_url.endswith('.git')
182
        if len(repo_url.split('/')) == 2 and "git@" not in repo_url:
183
            url = "https://github.com/{}".format(repo_url)
184
        else:
185
            url = repo_url
186
        return url if has_git_extension else "{}.git".format(url)
187
188
    @staticmethod
189
    def _eval_repo_name(repo_url):
190
        """
191
        Evaluate the name of the repo.
192
        https://github.com/StackStorm/st2contrib.git -> st2contrib
193
        https://github.com/StackStorm/st2contrib -> st2contrib
194
        [email protected]:StackStorm/st2contrib.git -> st2contrib
195
        [email protected]:StackStorm/st2contrib -> st2contrib
196
        """
197
        last_forward_slash = repo_url.rfind('/')
198
        next_dot = repo_url.find('.', last_forward_slash)
199
        # If dot does not follow last_forward_slash return till the end
200
        if next_dot < last_forward_slash:
201
            return repo_url[last_forward_slash + 1:]
202
        return repo_url[last_forward_slash + 1:next_dot]
203