001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *      http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.apache.oozie.command.bundle;
020
021import java.io.IOException;
022import java.io.StringReader;
023import java.util.Date;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Map.Entry;
028
029import org.apache.hadoop.conf.Configuration;
030import org.apache.oozie.BundleActionBean;
031import org.apache.oozie.BundleJobBean;
032import org.apache.oozie.ErrorCode;
033import org.apache.oozie.XException;
034import org.apache.oozie.action.hadoop.OozieJobInfo;
035import org.apache.oozie.client.Job;
036import org.apache.oozie.client.OozieClient;
037import org.apache.oozie.client.rest.JsonBean;
038import org.apache.oozie.command.CommandException;
039import org.apache.oozie.command.PreconditionException;
040import org.apache.oozie.command.StartTransitionXCommand;
041import org.apache.oozie.command.coord.CoordSubmitXCommand;
042import org.apache.oozie.executor.jpa.BatchQueryExecutor;
043import org.apache.oozie.executor.jpa.BundleJobQueryExecutor;
044import org.apache.oozie.executor.jpa.BundleJobQueryExecutor.BundleJobQuery;
045import org.apache.oozie.executor.jpa.JPAExecutorException;
046import org.apache.oozie.executor.jpa.BatchQueryExecutor.UpdateEntry;
047import org.apache.oozie.util.ELUtils;
048import org.apache.oozie.util.JobUtils;
049import org.apache.oozie.util.LogUtils;
050import org.apache.oozie.util.ParamChecker;
051import org.apache.oozie.util.XConfiguration;
052import org.apache.oozie.util.XmlUtils;
053import org.jdom.Attribute;
054import org.jdom.Element;
055import org.jdom.JDOMException;
056
057/**
058 * The command to start Bundle job
059 */
060public class BundleStartXCommand extends StartTransitionXCommand {
061    private final String jobId;
062    private BundleJobBean bundleJob;
063
064    /**
065     * The constructor for class {@link BundleStartXCommand}
066     *
067     * @param jobId the bundle job id
068     */
069    public BundleStartXCommand(String jobId) {
070        super("bundle_start", "bundle_start", 1);
071        this.jobId = ParamChecker.notEmpty(jobId, "jobId");
072    }
073
074    /**
075     * The constructor for class {@link BundleStartXCommand}
076     *
077     * @param jobId the bundle job id
078     * @param dryrun true if dryrun is enable
079     */
080    public BundleStartXCommand(String jobId, boolean dryrun) {
081        super("bundle_start", "bundle_start", 1, dryrun);
082        this.jobId = ParamChecker.notEmpty(jobId, "jobId");
083    }
084
085    /* (non-Javadoc)
086     * @see org.apache.oozie.command.XCommand#getEntityKey()
087     */
088    @Override
089    public String getEntityKey() {
090        return jobId;
091    }
092
093    @Override
094    public String getKey() {
095        return getName() + "_" + jobId;
096    }
097
098    /* (non-Javadoc)
099     * @see org.apache.oozie.command.XCommand#isLockRequired()
100     */
101    @Override
102    protected boolean isLockRequired() {
103        return true;
104    }
105
106    @Override
107    protected void verifyPrecondition() throws CommandException, PreconditionException {
108        if (bundleJob.getStatus() != Job.Status.PREP) {
109            String msg = "Bundle " + bundleJob.getId() + " is not in PREP status. It is in : " + bundleJob.getStatus();
110            LOG.info(msg);
111            throw new PreconditionException(ErrorCode.E1100, msg);
112        }
113    }
114
115    @Override
116    public void loadState() throws CommandException {
117        try {
118            this.bundleJob = BundleJobQueryExecutor.getInstance().get(BundleJobQuery.GET_BUNDLE_JOB, jobId);
119            LogUtils.setLogInfo(bundleJob);
120            super.setJob(bundleJob);
121        }
122        catch (XException ex) {
123            throw new CommandException(ex);
124        }
125    }
126
127    /* (non-Javadoc)
128     * @see org.apache.oozie.command.StartTransitionXCommand#StartChildren()
129     */
130    @Override
131    public void StartChildren() throws CommandException {
132        LOG.debug("Started coord jobs for the bundle=[{0}]", jobId);
133        insertBundleActions();
134        startCoordJobs();
135        LOG.debug("Ended coord jobs for the bundle=[{0}]", jobId);
136    }
137
138    /* (non-Javadoc)
139     * @see org.apache.oozie.command.TransitionXCommand#notifyParent()
140     */
141    @Override
142    public void notifyParent() {
143    }
144
145    /* (non-Javadoc)
146     * @see org.apache.oozie.command.StartTransitionXCommand#performWrites()
147     */
148    @Override
149    public void performWrites() throws CommandException {
150        try {
151            BatchQueryExecutor.getInstance().executeBatchInsertUpdateDelete(insertList, updateList, null);
152        }
153        catch (JPAExecutorException e) {
154            throw new CommandException(e);
155        }
156    }
157
158    /**
159     * Insert bundle actions
160     *
161     * @throws CommandException thrown if failed to create bundle actions
162     */
163    @SuppressWarnings("unchecked")
164    private void insertBundleActions() throws CommandException {
165        if (bundleJob != null) {
166            Map<String, Boolean> map = new HashMap<String, Boolean>();
167            try {
168                Element bAppXml = XmlUtils.parseXml(bundleJob.getJobXml());
169                List<Element> coordElems = bAppXml.getChildren("coordinator", bAppXml.getNamespace());
170                for (Element elem : coordElems) {
171                    Attribute name = elem.getAttribute("name");
172                    Attribute critical = elem.getAttribute("critical");
173                    if (name != null) {
174                        if (map.containsKey(name.getValue())) {
175                            throw new CommandException(ErrorCode.E1304, name);
176                        }
177                        boolean isCritical = false;
178                        if (critical != null && Boolean.parseBoolean(critical.getValue())) {
179                            isCritical = true;
180                        }
181                        map.put(name.getValue(), isCritical);
182                    }
183                    else {
184                        throw new CommandException(ErrorCode.E1305);
185                    }
186                }
187            }
188            catch (JDOMException jex) {
189                throw new CommandException(ErrorCode.E1301, jex.getMessage(), jex);
190            }
191
192            // if there is no coordinator for this bundle, failed it.
193            if (map.isEmpty()) {
194                bundleJob.setStatus(Job.Status.FAILED);
195                bundleJob.resetPending();
196                try {
197                    BundleJobQueryExecutor.getInstance().executeUpdate(BundleJobQuery.UPDATE_BUNDLE_JOB_STATUS_PENDING, bundleJob);
198                }
199                catch (JPAExecutorException jex) {
200                    throw new CommandException(jex);
201                }
202
203                LOG.debug("No coord jobs for the bundle=[{0}], failed it!!", jobId);
204                throw new CommandException(ErrorCode.E1318, jobId);
205            }
206
207            for (Entry<String, Boolean> coordName : map.entrySet()) {
208                BundleActionBean action = createBundleAction(jobId, coordName.getKey(), coordName.getValue());
209                insertList.add(action);
210            }
211        }
212        else {
213            throw new CommandException(ErrorCode.E0604, jobId);
214        }
215    }
216
217    private BundleActionBean createBundleAction(String jobId, String coordName, boolean isCritical) {
218        BundleActionBean action = new BundleActionBean();
219        action.setBundleActionId(jobId + "_" + coordName);
220        action.setBundleId(jobId);
221        action.setCoordName(coordName);
222        action.setStatus(Job.Status.PREP);
223        action.setLastModifiedTime(new Date());
224        if (isCritical) {
225            action.setCritical();
226        }
227        else {
228            action.resetCritical();
229        }
230        return action;
231    }
232
233    /**
234     * Start Coord Jobs
235     *
236     * @throws CommandException thrown if failed to start coord jobs
237     */
238    @SuppressWarnings("unchecked")
239    private void startCoordJobs() throws CommandException {
240        if (bundleJob != null) {
241            try {
242                Element bAppXml = XmlUtils.parseXml(bundleJob.getJobXml());
243                List<Element> coordElems = bAppXml.getChildren("coordinator", bAppXml.getNamespace());
244                for (Element coordElem : coordElems) {
245                    Attribute name = coordElem.getAttribute("name");
246
247                    Configuration coordConf = mergeConfig(coordElem);
248                    coordConf.set(OozieClient.BUNDLE_ID, jobId);
249                    if (OozieJobInfo.isJobInfoEnabled()) {
250                        coordConf.set(OozieJobInfo.BUNDLE_NAME, bundleJob.getAppName());
251                    }
252                    String coordName=name.getValue();
253                    try {
254                        coordName = ELUtils.resolveAppName(coordName, coordConf);
255                    }
256                    catch (Exception e) {
257                        throw new CommandException(ErrorCode.E1321, e.getMessage(), e);
258
259                    }
260                    queue(new CoordSubmitXCommand(coordConf, bundleJob.getId(), name.getValue()));
261
262                }
263                updateBundleAction();
264            }
265            catch (JDOMException jex) {
266                throw new CommandException(ErrorCode.E1301, jex.getMessage(), jex);
267            }
268            catch (JPAExecutorException je) {
269                throw new CommandException(je);
270            }
271        }
272        else {
273            throw new CommandException(ErrorCode.E0604, jobId);
274        }
275    }
276
277    private void updateBundleAction() throws JPAExecutorException {
278        for(JsonBean bAction : insertList) {
279            BundleActionBean action = (BundleActionBean) bAction;
280            action.incrementAndGetPending();
281            action.setLastModifiedTime(new Date());
282        }
283    }
284
285    /**
286     * Merge Bundle job config and the configuration from the coord job to pass
287     * to Coord Engine
288     *
289     * @param coordElem the coordinator configuration
290     * @return Configuration merged configuration
291     * @throws CommandException thrown if failed to merge configuration
292     */
293    private Configuration mergeConfig(Element coordElem) throws CommandException {
294        String jobConf = bundleJob.getConf();
295        // Step 1: runConf = jobConf
296        Configuration runConf = null;
297        try {
298            runConf = new XConfiguration(new StringReader(jobConf));
299        }
300        catch (IOException e1) {
301            LOG.warn("Configuration parse error in:" + jobConf);
302            throw new CommandException(ErrorCode.E1306, e1.getMessage(), e1);
303        }
304        // Step 2: Merge local properties into runConf
305        // extract 'property' tags under 'configuration' block in the coordElem
306        // convert Element to XConfiguration
307        Element localConfigElement = coordElem.getChild("configuration", coordElem.getNamespace());
308
309        if (localConfigElement != null) {
310            String strConfig = XmlUtils.prettyPrint(localConfigElement).toString();
311            Configuration localConf;
312            try {
313                localConf = new XConfiguration(new StringReader(strConfig));
314            }
315            catch (IOException e1) {
316                LOG.warn("Configuration parse error in:" + strConfig);
317                throw new CommandException(ErrorCode.E1307, e1.getMessage(), e1);
318            }
319
320            // copy configuration properties in the coordElem to the runConf
321            XConfiguration.copy(localConf, runConf);
322        }
323
324        // Step 3: Extract value of 'app-path' in coordElem, save it as a
325        // new property called 'oozie.coord.application.path', and normalize.
326        String appPath = coordElem.getChild("app-path", coordElem.getNamespace()).getValue();
327        runConf.set(OozieClient.COORDINATOR_APP_PATH, appPath);
328        // Normalize coordinator appPath here;
329        try {
330            JobUtils.normalizeAppPath(runConf.get(OozieClient.USER_NAME), runConf.get(OozieClient.GROUP_NAME), runConf);
331        }
332        catch (IOException e) {
333            throw new CommandException(ErrorCode.E1001, runConf.get(OozieClient.COORDINATOR_APP_PATH));
334        }
335        return runConf;
336    }
337
338    /* (non-Javadoc)
339     * @see org.apache.oozie.command.TransitionXCommand#getJob()
340     */
341    @Override
342    public Job getJob() {
343        return bundleJob;
344    }
345
346    /* (non-Javadoc)
347     * @see org.apache.oozie.command.TransitionXCommand#updateJob()
348     */
349    @Override
350    public void updateJob() throws CommandException {
351        updateList.add(new UpdateEntry<BundleJobQuery>(BundleJobQuery.UPDATE_BUNDLE_JOB_STATUS_PENDING, bundleJob));
352    }
353}